Aprendizaje automático y Data Science (Parte 06). Redes neuronales (Parte 02): arquitectura de la redes neuronales con conexión directa

Omega J Msigwa | 24 noviembre, 2022

«No afirmo que las redes neuronales sean fáciles. Hace falta un experto para que funcionen, pero la experiencia tiene una aplicación mucho más amplia. El esfuerzo que antes se dedicaba a desarrollar las funciones, ahora se dedica a desarrollar la arquitectura, la función de pérdida y el esquema de optimización. Así, el trabajo manual pasa a un nivel superior de abstracción».

-Stefano Soatto


Introducción

En el artículo anterior, hablamos de los fundamentos de las redes neuronales y construimos un perceptrón multicapa (MLP) muy sencillo y estático. Eso sí, somos conscientes de que en las aplicaciones del mundo real resulta poco probable que tengamos que usar dos parámetros de entrada elementales y dos nodos de capa oculta, que es con lo que trabajamos la última vez.

Las aplicaciones específicas pueden requerir redes con 10 nodos en la capa de entrada, 13 nodos/neuronas en la capa oculta y, digamos, cuatro en la capa de salida. En este caso, deberíamos reconfigurar toda la red neuronal, 

lo cual significa que necesitaremos una solución dinámica; un código dinámico cuyos parámetros pueden modificarse y optimizarse sin influir en el programa. Si utilizamos la biblioteca python keras para construir las redes neuronales, tendremos menos trabajo a la hora de configurar e incluso crear las arquitecturas más complejas. Esto es exactamente lo que queremos conseguir con MQL5.

En el artículo "Regresión lineal (Parte 3)", cuya lectura recomendamos encarecidamente, presentamos un formulario matricial-vectorial que permite crear modelos flexibles con un número ilimitado de datos de entrada.

Matrices al rescate

Si por cualquier motivo debemos cambiar los parámetros de un modelo con código estático, la optimización puede ocupar mucho tiempo: entre otras lindezas, acaba siendo un auténtico quebradero de cabeza y un dolor de espalda considerable.

Representación de una red neuronal


Si observamos con más detenimiento las operaciones subyacentes a la red neuronal, veremos que cada entrada se multiplica por su coeficiente de peso asignado, y el resultado se suma al desplazamiento. Esto se gestiona bien con la ayuda de operaciones matriciales.

Multiplicación de matrices en una red neuronal

En esencia, encontramos el producto escalar de los datos de entrada y la matriz de coeficientes de peso, y luego lo añadimos al desplazamiento.

Para construir una red neuronal flexible, intentaremos trabajar con una arquitectura impar de dos nodos en la capa de entrada, cuatro en la primera capa oculta, seis en la segunda y uno en la tercera, y con un nodo en la capa de salida.

Arquitectura de una red neuronal

Esto nos permitirá probar si la lógica matricial funcionará en todos los escenarios necesarios:

Antes de comenzar a escribir el código para las operaciones matriciales y el cálculo de los valores, implementaremos las cosas básicas necesarias sin las que el trabajo resultaría imposible.

Generación de pesos y valores de desplazamiento aleatorios

    //Generate random bias
    for(int i=0; i<m_hiddenLayers; i++)         bias[i] = MathRandom(0,1);
    
    //generate weights 
    int sum_weights=0, L_inputs=inputs;
    double L_weights[];
    
    for (int i=0; i<m_hiddenLayers; i++)
      {
         sum_weights += L_inputs * m_hiddenLayerNodes[i];
         ArrayResize(Weights,sum_weights);
         L_inputs = m_hiddenLayerNodes[i];         
      }
    
    for (int j=0; j<sum_weights; j++) Weights[j] = MathRandom(0,1);

Ya hemos trabajado con esta operación en el apartado anterior. Debemos tener en cuenta que estos pesos y desplazamientos deberán generarse una vez y utilizarse en el ciclo de épocas.


¿Qué es una época?

Una época es una pasada completa de todos los datos de una red neuronal. En el caso de feed-forward, se trata de una pasada directa completa de todos los datos de entrada, en back propagation, se trata de una pasada directa e inversa completa. En palabras simples, hablamos de una época cuando la red neuronal ha visto todos los datos.

A diferencia del perceptrón multicapa del que hablamos en el artículo anterior, encontraremos una implementación que tenga en cuenta la función de activación en la capa de salida. Quienes utilizan Keras, probablemente estén familiarizados con ellas. De hecho, podemos tener cualquier función de activación en la capa oculta que conduzca a un resultado en la capa de salida.

CNeuralNets(fx HActivationFx,fx OActivationFx,int &NodesHL[],int outputs=NULL, bool SoftMax=false);

Preste atención a los parámetros de entrada: HActivationFx — función de activación en las capas ocultas, OActivationFx — función de activación en la capa de salida, NodesHL[] — número de nodos en la capa oculta. Si un array tiene, digamos, 3 elementos, esto significará que tendrá 3 capas ocultas, y el número de nodos en estas capas estará determinado por los elementos presentes en el array. Veamos el código siguiente:

int hlnodes[3] = {4,6,1};
int outputs = 1;
     
neuralnet = new CNeuralNets(SIGMOID,RELU,hlnodes,outputs);

Esta es la primera arquitectura mostrada más arriba. El parámetro outputs es opcional. Si lo dejamos en NULL, se aplicará la siguiente configuración a la capa de salida:

if (m_outputLayers == NULL)
{
  if (A_fx == RELU)     m_outputLayers = 1;
  else                  m_outputLayers = ArraySize(MLPInputs);
}

Si seleccionamos la función de activación RELU en la capa oculta, habrá un nodo en la capa de salida. De lo contrario, el número de salidas en la capa final será igual al número de entradas en la primera capa. Si la capa oculta utiliza una función de activación distinta a RELU, es probable que se trate de una red neuronal de clasificación, por lo que, en ese caso, la capa de salida se ajustará por defecto al número de columnas. En realidad, esto no es del todo correcto: los resultados deberían darse según el número de funciones objetivo del conjunto de datos cuando se trata de tareas de clasificación. En futuras versiones encontraremos la manera de cambiar este comportamiento, pero por ahora deberemos seleccionar el número de neuronas de salida manualmente.

Ahora llamaremos a la función completa del perceptrón multicapa (MLP) y veremos el resultado. A continuación, explicaremos lo que se ha hecho para conseguirlo.

LI      0       10:10:29.995    NNTestScript (#NQ100,H1)        CNeural Nets Initialized activation = SIGMOID UseSoftMax = No
IF      0       10:10:29.995    NNTestScript (#NQ100,H1)        biases
EI      0       10:10:29.995    NNTestScript (#NQ100,H1)        0.6283 0.2029 0.1004
IQ      0       10:10:29.995    NNTestScript (#NQ100,H1)        Hidden Layer 1 | Nodes 4 | Bias 0.6283
NS      0       10:10:29.995    NNTestScript (#NQ100,H1)        Inputs 2 Weights 8
JD      0       10:10:29.995    NNTestScript (#NQ100,H1)        4.00000 6.00000
FL      0       10:10:29.995    NNTestScript (#NQ100,H1)        0.954 0.026 0.599 0.952 0.864 0.161 0.818 0.765
EJ      0       10:10:29.995    NNTestScript (#NQ100,H1)        Arr size A 2
EM      0       10:10:29.995    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 3.81519 X A[0] = 4.000 B[0] = 0.954
NI      0       10:10:29.995    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 9.00110 X A[1] = 6.000 B[4] = 0.864
IE      0       10:10:29.995    NNTestScript (#NQ100,H1)        AxBMatrix[1] = 0.10486 X A[0] = 4.000 B[1] = 0.026
DQ      0       10:10:29.995    NNTestScript (#NQ100,H1)        AxBMatrix[1] = 1.06927 X A[1] = 6.000 B[5] = 0.161
MM      0       10:10:29.995    NNTestScript (#NQ100,H1)        AxBMatrix[2] = 2.39417 X A[0] = 4.000 B[2] = 0.599
JI      0       10:10:29.995    NNTestScript (#NQ100,H1)        AxBMatrix[2] = 7.29974 X A[1] = 6.000 B[6] = 0.818
GE      0       10:10:29.995    NNTestScript (#NQ100,H1)        AxBMatrix[3] = 3.80725 X A[0] = 4.000 B[3] = 0.952
KQ      0       10:10:29.995    NNTestScript (#NQ100,H1)        AxBMatrix[3] = 8.39569 X A[1] = 6.000 B[7] = 0.765
DL      0       10:10:29.995    NNTestScript (#NQ100,H1)        before rows 1 cols 4
GI      0       10:10:29.995    NNTestScript (#NQ100,H1)        IxWMatrix
QM      0       10:10:29.995    NNTestScript (#NQ100,H1)        Matrix
CH      0       10:10:29.995    NNTestScript (#NQ100,H1)        [ 
HK      0       10:10:29.995    NNTestScript (#NQ100,H1)        9.00110 1.06927 7.29974 8.39569
OO      0       10:10:29.995    NNTestScript (#NQ100,H1)        ] 
CH      0       10:10:29.995    NNTestScript (#NQ100,H1)        rows = 1 cols = 4

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< End of the first Hidden Layer >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

NS      0       10:10:29.995    NNTestScript (#NQ100,H1)        Hidden Layer 2 | Nodes 6 | Bias 0.2029
HF      0       10:10:29.995    NNTestScript (#NQ100,H1)        Inputs 4 Weights 24
LR      0       10:10:29.995    NNTestScript (#NQ100,H1)        0.99993 0.84522 0.99964 0.99988
EL      0       10:10:29.996    NNTestScript (#NQ100,H1)        0.002 0.061 0.056 0.600 0.737 0.454 0.113 0.622 0.387 0.456 0.938 0.587 0.379 0.207 0.356 0.784 0.046 0.597 0.511 0.838 0.848 0.748 0.047 0.282
FF      0       10:10:29.996    NNTestScript (#NQ100,H1)        Arr size A 4
EI      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 0.00168 X A[0] = 1.000 B[0] = 0.002
QE      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 0.09745 X A[1] = 0.845 B[6] = 0.113
MR      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 0.47622 X A[2] = 1.000 B[12] = 0.379
NN      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 0.98699 X A[3] = 1.000 B[18] = 0.511
MI      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[1] = 0.06109 X A[0] = 1.000 B[1] = 0.061
ME      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[1] = 0.58690 X A[1] = 0.845 B[7] = 0.622
PR      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[1] = 0.79347 X A[2] = 1.000 B[13] = 0.207
KN      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[1] = 1.63147 X A[3] = 1.000 B[19] = 0.838
GI      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[2] = 0.05603 X A[0] = 1.000 B[2] = 0.056
GE      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[2] = 0.38353 X A[1] = 0.845 B[8] = 0.387
GS      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[2] = 0.73961 X A[2] = 1.000 B[14] = 0.356
CO      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[2] = 1.58725 X A[3] = 1.000 B[20] = 0.848
KH      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[3] = 0.59988 X A[0] = 1.000 B[3] = 0.600
OD      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[3] = 0.98514 X A[1] = 0.845 B[9] = 0.456
LS      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[3] = 1.76888 X A[2] = 1.000 B[15] = 0.784
KO      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[3] = 2.51696 X A[3] = 1.000 B[21] = 0.748
PH      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[4] = 0.73713 X A[0] = 1.000 B[4] = 0.737
FG      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[4] = 1.53007 X A[1] = 0.845 B[10] = 0.938
RS      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[4] = 1.57626 X A[2] = 1.000 B[16] = 0.046
OO      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[4] = 1.62374 X A[3] = 1.000 B[22] = 0.047
EH      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[5] = 0.45380 X A[0] = 1.000 B[5] = 0.454
DG      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[5] = 0.95008 X A[1] = 0.845 B[11] = 0.587
PS      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[5] = 1.54675 X A[2] = 1.000 B[17] = 0.597
EO      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[5] = 1.82885 X A[3] = 1.000 B[23] = 0.282
KH      0       10:10:29.996    NNTestScript (#NQ100,H1)        before rows 1 cols 6
RL      0       10:10:29.996    NNTestScript (#NQ100,H1)        IxWMatrix
HI      0       10:10:29.996    NNTestScript (#NQ100,H1)        Matrix
NS      0       10:10:29.996    NNTestScript (#NQ100,H1)        [ 
ND      0       10:10:29.996    NNTestScript (#NQ100,H1)        0.98699 1.63147 1.58725 2.51696 1.62374 1.82885
JM      0       10:10:29.996    NNTestScript (#NQ100,H1)        ] 
LG      0       10:10:29.996    NNTestScript (#NQ100,H1)        rows = 1 cols = 6

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< End of second Hidden Layer >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

ML      0       10:10:29.996    NNTestScript (#NQ100,H1)        Hidden Layer 3 | Nodes 1 | Bias 0.1004
OG      0       10:10:29.996    NNTestScript (#NQ100,H1)        Inputs 6 Weights 6
NQ      0       10:10:29.996    NNTestScript (#NQ100,H1)        0.76671 0.86228 0.85694 0.93819 0.86135 0.88409
QM      0       10:10:29.996    NNTestScript (#NQ100,H1)        0.278 0.401 0.574 0.301 0.256 0.870
RD      0       10:10:29.996    NNTestScript (#NQ100,H1)        Arr size A 6
NO      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 0.21285 X A[0] = 0.767 B[0] = 0.278
QK      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 0.55894 X A[1] = 0.862 B[1] = 0.401
CG      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 1.05080 X A[2] = 0.857 B[2] = 0.574
DS      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 1.33314 X A[3] = 0.938 B[3] = 0.301
HO      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 1.55394 X A[4] = 0.861 B[4] = 0.256
CJ      0       10:10:29.996    NNTestScript (#NQ100,H1)        AxBMatrix[0] = 2.32266 X A[5] = 0.884 B[5] = 0.870
HF      0       10:10:29.996    NNTestScript (#NQ100,H1)        before rows 1 cols 1
LR      0       10:10:29.996    NNTestScript (#NQ100,H1)        IxWMatrix
NS      0       10:10:29.996    NNTestScript (#NQ100,H1)        Matrix
DF      0       10:10:29.996    NNTestScript (#NQ100,H1)        [ 
NN      0       10:10:29.996    NNTestScript (#NQ100,H1)        2.32266
DJ      0       10:10:29.996    NNTestScript (#NQ100,H1)        ] 
GM      0       10:10:29.996    NNTestScript (#NQ100,H1)        rows = 1 cols = 1

La siguiente imagen es una representación de la red. Muestra lo que hemos hecho solo en la primera capa, el resto es solo una repetición del mismo procedimiento.

Funcionamiento de una red neuronal monocapa

Como era de esperar, la multiplicación matricial aquí multiplica los pesos de la primera capa por los datos de entrada. Sin embargo, programar la lógica no ha resultado tan sencillo. Aquí uno puede confundirse, mire el código de abajo. Observe la función MatrixMultiply; puede ignorar el resto del código por ahora.

void   CNeuralNets::FeedForwardMLP(
                    double &MLPInputs[],
                    double &MLPOutput[])
 {    
//---

    m_hiddenLayers = m_hiddenLayers+1;
    
    ArrayResize(m_hiddenLayerNodes,m_hiddenLayers);    
    m_hiddenLayerNodes[m_hiddenLayers-1] = m_outputLayers;
         
    int HLnodes = ArraySize(MLPInputs); 
    int weight_start = 0;
    
    double Weights[], bias[];
    ArrayResize(bias,m_hiddenLayers);
    
//---
    
    int inputs=ArraySize(MLPInputs);
    int w_size = 0; //size of weights

    int cols = inputs, rows=1;
    
    double IxWMatrix[]; //dot product matrix 
    
    //Generate random bias
    for(int i=0; i<m_hiddenLayers; i++)         bias[i] = MathRandom(0,1);
         
    //generate weights 
    int sum_weights=0, L_inputs=inputs;
    double L_weights[];
    
    for (int i=0; i<m_hiddenLayers; i++)
      {
         sum_weights += L_inputs * m_hiddenLayerNodes[i];
         ArrayResize(Weights,sum_weights);
         L_inputs = m_hiddenLayerNodes[i];         
      }
    
    for (int j=0; j<sum_weights; j++) Weights[j] = MathRandom(0,1);
                        
    for (int i=0; i<m_hiddenLayers; i++)
      {          
          w_size = (inputs*m_hiddenLayerNodes[i]);
          ArrayResize(L_weights,w_size);
                     
            ArrayCopy(L_weights,Weights,0,0,w_size);
            ArrayRemove(Weights,0,w_size); 
             
              MatrixMultiply(MLPInputs,L_weights,IxWMatrix,cols,cols,rows,cols);
               
              
              ArrayFree(MLPInputs); ArrayResize(MLPInputs,m_hiddenLayerNodes[i]);
              inputs = ArraySize(MLPInputs); 
              
              for(int k=0; k<ArraySize(IxWMatrix); k++) MLPInputs[k] = ActivationFx(IxWMatrix[k]+bias[i]); 
               
      } 
 } 

La primera entrada a la red en la capa de entrada es una matriz 1xn, lo cual significa que tiene una fila y un número desconocido de columnas (n). Inicializamos esta lógica antes del ciclo for en la línea

  int cols = inputs, rows=1;

para obtener así el número total de pesos necesarios para completar el proceso de multiplicación. Luego multiplicamos el número de nodos de la capa de entrada/anterior por el número de la capa de salida/siguiente. En este caso, tenemos 2 entradas y 4 nodos en la primera capa oculta, por lo que necesitaremos 2x4 = 8 valores de pesos. El truco más importante de todos se muestra abajo:

 MatrixMultiply(MLPInputs,L_weights,IxWMatrix,cols,cols,rows,cols);

Para entenderlo mejor, vamos a ver qué hace la multiplicación de matrices:

void MatrixMultiply(double &A[],double &B[],double &AxBMatrix[], int colsA,int rowsB,int &new_rows,int &new_cols)

Los últimos parámetros new_rows y new_cols son los valores actualizados del número de filas y columnas de la nueva matriz. Los valores se reutilizan como número de filas y columnas para la matriz siguiente. ¿Recuerda que la entrada para la siguiente capa son los valores de salida de la capa anterior?

Esto resulta aún más importante para la matriz porque

Sabemos que para multiplicar matrices, el número de columnas de la primera matriz deberá ser igual al número de filas de la segunda. La matriz resultante tendrá la dimensionalidad según el número de filas de la primera matriz y el número de columnas de la segunda matriz.

Usando como base las operaciones anteriores

Los primeros parámetros de entrada son aquellos cuyas dimensiones ya conocemos, pero la matriz de coeficientes de peso tiene 8 elementos que se han obtenido multiplicando los datos de entrada y el número de nodos de la capa oculta, por lo que podemos concluir que tiene filas iguales al número de columnas de la capa anterior/de entrada, y eso es prácticamente todo. Esta lógica resulta posible gracias al proceso de cambio de los valores de las nuevas filas y las nuevas columnas por los antiguos (dentro de la función de multiplicación de matrices)

 new_rows = rowsA;  new_cols = colsB;

Podrá encontrar más información sobre las matrices en la biblioteca estándar. No obstante lo dicho, si le interesa algo más fuera de la biblioteca, encontrará un enlace al final del artículo.

Bien, tenemos una arquitectura flexible. Veamos ahora cómo podrían ser el entrenamiento y la prueba de la red en el caso de un perceptrón MLP multicapa con conexión directa.

Procesos implicados

  1. Vamos a entrenar la red durante x épocas y a encontrar el modelo con menos errores.
  2. Para ello, guardaremos los parámetros del modelo en un archivo binario que podrá abrirse en otros programas, por ejemplo, dentro del asesor.

Espere un segundo, ¿acabo de decir que encontramos el modelo con menos errores? En realidad no, se trata simplemente de un enfoque directo.

A algunos miembros de MQL5.community les gusta optimizar los asesores con estos parámetros de entrada. Esto funciona, pero, en este caso, los pesos y los desplazamientos se generan solo una vez y luego se utilizan para el resto de las épocas, al igual que en la pasada inversa, salvo que estos valores no se actualizan.

Utilizamos un número de épocas por defecto igual a 1.

void CNeuralNets::train_feedforwardMLP(double &XMatrix[],int epochs=1)

Puede intentar cambiar el código para transmitir los coeficientes de peso a la entrada del script, desde ahí también podrá establecer cualquier valor para el número de épocas. Existen muchas otras formas.


Probamos o usamos el modelo con nuevos datos

Para poder usar el modelo que hemos entrenado, necesitaremos poder compartir sus parámetros con otros programas. Esto podrá hacerse a través de los archivos. Como los parámetros de nuestro modelo son valores double de arrays, necesitaremos archivos binarios. Leemos los archivos en los que se almacenan los coeficientes de peso y los desplazamientos y los almacenamos en sus respectivas matrices para su uso.

Esta es la función responsable del entrenamiento de la red neuronal.

void CNeuralNets::train_feedforwardMLP(double &XMatrix[],int epochs=1)
   {         
      double MLPInputs[]; ArrayResize(MLPInputs,m_inputs);
      double MLPOutputs[]; ArrayResize(MLPOutputs,m_outputLayers);
      
      double Weights[], bias[];
      
      setmodelParams(Weights,bias); //Generating random weights and bias
      
      for (int i=0; i<epochs; i++)
         {
           int start = 0;
           int rows = ArraySize(XMatrix)/m_inputs;
           
               {
                 if (m_debug) printf("<<<< %d >>>",j+1);
                 ArrayCopy(MLPInputs,XMatrix,0,start,m_inputs);
             
                 FeedForwardMLP(MLPInputs,MLPOutputs,Weights,bias);
             
                 start+=m_inputs;
               }
         }
       
       WriteBin(Weights,bias);
   }

La función setmodelParams() genera valores aleatorios para los coeficientes de peso y los desplazamientos. Después de entrenar el modelo, obtendremos los coeficientes de peso y los desplazamientos y los guardaremos en un archivo binario.

WriteBin(Weights,bias);

Para ver cómo funcionan las cosas en MLP, utilizaremos el conjunto de datos del ejemplo real de aquí

El parámetro XMatrix[] supone una matriz con todos los valores de entrada con los que queremos entrenar nuestro modelo. En nuestro caso, deberemos importar el archivo CSV a una matriz.

nasdaq dataset


Importamos el conjunto de datos

     double XMatrix[]; int rows,cols;
     
     CSVToMatrix(XMatrix,rows,cols,"NASDAQ_DATA.csv");
     MatrixPrint(XMatrix,cols,3);

La salida de ese código sería:

MN      0       12:02:13.339    NNTestScript (#NQ100,H1)        Matrix
MI      0       12:02:13.340    NNTestScript (#NQ100,H1)        [ 
MJ      0       12:02:13.340    NNTestScript (#NQ100,H1)         4173.800 13067.500 13386.600    34.800
RD      0       12:02:13.340    NNTestScript (#NQ100,H1)         4179.200 13094.800 13396.700    36.600
JQ      0       12:02:13.340    NNTestScript (#NQ100,H1)         4182.700 13108.000 13406.600    37.500
FK      0       12:02:13.340    NNTestScript (#NQ100,H1)         4185.800 13104.300 13416.800    37.100
.....
.....
.....
DK      0       12:02:13.353    NNTestScript (#NQ100,H1)         4332.700 14090.200 14224.600    43.700
GD      0       12:02:13.353    NNTestScript (#NQ100,H1)         4352.500 14162.000 14225.000    47.300
IN      0       12:02:13.353    NNTestScript (#NQ100,H1)         4401.900 14310.300 14226.200    56.100
DK      0       12:02:13.353    NNTestScript (#NQ100,H1)         4405.200 14312.700 14224.500    56.200
EE      0       12:02:13.353    NNTestScript (#NQ100,H1)         4415.800 14370.400 14223.200    60.000
OS      0       12:02:13.353    NNTestScript (#NQ100,H1)        ] 
IE      0       12:02:13.353    NNTestScript (#NQ100,H1)        rows = 744 cols = 4

El archivo CSV completo se almacenará ahora dentro de XMatrix[]. ¡Hurra!

La ventaja de la matriz resultante es que ya no debemos preocuparnos por los datos de entrada de la red neuronal, ya que la variable cols obtiene el número de columnas del archivo CSV. Estas serán las entradas de la red neuronal, y este será el aspecto del script completo:

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+

#include "NeuralNets.mqh";
CNeuralNets *neuralnet; 

//+------------------------------------------------------------------+
void OnStart()
  {
     
     int hlnodes[3] = {4,6,1};
     int outputs = 1;
     int inputs_=2;
     
     double XMatrix[]; int rows,cols;
     
     CSVToMatrix(XMatrix,rows,cols,"NASDAQ_DATA.csv");
     MatrixPrint(XMatrix,cols,3);
     
     neuralnet = new CNeuralNets(SIGMOID,RELU,cols,hlnodes,outputs);     
     
     neuralnet.train_feedforwardMLP(XMatrix);
     
     delete(neuralnet);
    
  }

Simple, ¿verdad? No obstante, debemos corregir algunas líneas de código. En Train_feedforwardMLP se añaden iteraciones de todo el conjunto de datos a una única iteración de época.

       for (int i=0; i<epochs; i++)
         {
           int start = 0;
           int rows = ArraySize(XMatrix)/m_inputs;
           
             for (int j=0; j<rows; j++) //iterate the entire dataset in a single epoch
               {
                 if (m_debug) printf("<<<< %d >>>",j+1);
                 ArrayCopy(MLPInputs,XMatrix,0,start,m_inputs);
             
                 FeedForwardMLP(MLPInputs,MLPOutputs,Weights,bias);
             
                 start+=m_inputs;
               }
         }

Vamos a comprobar los registros al ejecutar el programa en modo de depuración.

bool m_debug = true; 

El modo de depuración ocupa mucho espacio en el disco, por lo tanto, para cualquier otro uso (no en el modo de depuración), lo estableceremos en false. Tras una sola ejecución del programa, nuestros registros tenían un tamaño de 21Mb.

Un breve resumen de las dos iteraciones:

MR      0       12:23:16.485    NNTestScript (#NQ100,H1)        <<<< 1 >>>
DE      0       12:23:16.485    NNTestScript (#NQ100,H1)        Hidden layer nodes plus the output
FS      0       12:23:16.485    NNTestScript (#NQ100,H1)        4 6 1 1
KK      0       12:23:16.485    NNTestScript (#NQ100,H1)        Hidden Layer 1 | Nodes 4 | Bias 0.3903
IN      0       12:23:16.485    NNTestScript (#NQ100,H1)        Inputs 4 Weights 16
MJ      0       12:23:16.485    NNTestScript (#NQ100,H1)         4173.80000 13067.50000 13386.60000    34.80000
DF      0       12:23:16.485    NNTestScript (#NQ100,H1)        0.060 0.549 0.797 0.670 0.420 0.914 0.146 0.968 0.464 0.031 0.855 0.240 0.717 0.288 0.372 0.805
....
PD      0       12:23:16.485    NNTestScript (#NQ100,H1)        MLP Final Output
LM      0       12:23:16.485    NNTestScript (#NQ100,H1)        1.333
HP      0       12:23:16.485    NNTestScript (#NQ100,H1)        <<<< 2 >>>
PG      0       12:23:16.485    NNTestScript (#NQ100,H1)        Hidden layer nodes plus the output
JR      0       12:23:16.485    NNTestScript (#NQ100,H1)        4 6 1 1
OH      0       12:23:16.485    NNTestScript (#NQ100,H1)        Hidden Layer 1 | Nodes 4 | Bias 0.3903
EI      0       12:23:16.485    NNTestScript (#NQ100,H1)        Inputs 4 Weights 16
FM      0       12:23:16.485    NNTestScript (#NQ100,H1)         4179.20000 13094.80000 13396.70000    36.60000
II      0       12:23:16.486    NNTestScript (#NQ100,H1)        0.060 0.549 0.797 0.670 0.420 0.914 0.146 0.968 0.464 0.031 0.855 0.240 0.717 0.288 0.372 0.805
GJ      0       12:23:16.486    NNTestScript (#NQ100,H1)        

Ya está todo configurado, funciona tan bien como se esperaba. Ahora guardaremos los parámetros del modelo en un archivo binario.

Guardamos los parámetros del modelo en un archivo

bool CNeuralNets::WriteBin(double &w[], double &b[])
 {
      string file_name_w = NULL, file_name_b=  NULL;
      int handle_w, handle_b;
      
      file_name_w = MQLInfoString(MQL_PROGRAM_NAME)+"\\"+"model_w.bin";
      file_name_b =  MQLInfoString(MQL_PROGRAM_NAME)+"\\"+"model_b.bin"; 
      
      FileDelete(file_name_w); FileDelete(file_name_b);
      
       handle_w = FileOpen(file_name_w,FILE_WRITE|FILE_BIN);       
       if (handle_w == INVALID_HANDLE)   {  printf("Invalid %s Handle err %d",file_name_w,GetLastError());  }
       else                                 FileWriteArray(handle_w,w);
      
       FileClose(handle_w);    
       
       handle_b = FileOpen(file_name_b,FILE_WRITE|FILE_BIN);
       if (handle_b == INVALID_HANDLE)   {  printf("Invalid %s Handle err %d",file_name_b,GetLastError());  }
       else                                 FileWriteArray(handle_b,b);
     
       FileClose(handle_b);
     
     return(true);
 }


Este paso tiene una gran importancia. Como ya hemos dicho, esto ayuda a compartir los parámetros del modelo con otros programas que usan la misma biblioteca. Los archivos se guardarán en una subcarpeta que tendrá el mismo nombre que el archivo de script:

Archivos del modelo de red neuronal mql5

Ejemplo de acceso a los parámetros del modelo en otros programas:

     double weights[], bias[];
     
     int handlew = FileOpen("NNTestScript\\model_w.bin",FILE_READ|FILE_BIN);
     FileReadArray(handlew,weights);
     FileClose(handlew);
     
     int handleb = FileOpen("NNTestScript\\model_b.bin",FILE_READ|FILE_BIN);
     FileReadArray(handleb,bias);
     FileClose(handleb);
     
     Print("bias"); ArrayPrint(bias,4);
     Print("Weights"); ArrayPrint(weights,4);

Resultado

HR      0       14:14:02.380    NNTestScript (#NQ100,H1)        bias
DG      0       14:14:02.385    NNTestScript (#NQ100,H1)        0.0063 0.2737 0.9216 0.4435
OQ      0       14:14:02.385    NNTestScript (#NQ100,H1)        Weights
GG      0       14:14:02.385    NNTestScript (#NQ100,H1)        [ 0] 0.5338 0.6378 0.6710 0.6256 0.8313 0.8093 0.1779 0.4027 0.5229 0.9181 0.5449 0.4888 0.9003 0.2870 0.7107 0.8477
NJ      0       14:14:02.385    NNTestScript (#NQ100,H1)        [16] 0.2328 0.1257 0.4917 0.1930 0.3924 0.2824 0.4536 0.9975 0.9484 0.5822 0.0198 0.7951 0.3904 0.7858 0.7213 0.0529
EN      0       14:14:02.385    NNTestScript (#NQ100,H1)        [32] 0.6332 0.6975 0.9969 0.3987 0.4623 0.4558 0.4474 0.4821 0.0742 0.5364 0.9512 0.2517 0.3690 0.4989 0.5482

Podremos acceder a los archivos desde cualquier lugar, solo tendremos que conocer el nombre y la ubicación.

Uso del modelo

Esta ha sido la parte fácil. La función de pasada directa de MLP ha cambiado: hemos añadido nuevos pesos de entrada y desplazamientos, esto nos ayudará, por ejemplo, al ejecutar el modelo para trabajar con los datos de precio recientes o algo más.

void   CNeuralNets::FeedForwardMLP(double &MLPInputs[],double &MLPOutput[],double &Weights[], double &bias[])

Al final del código, extraeremos los pesos y los desplazamientos y utilizaremos el modelo en la práctica. Primero leeremos los parámetros, luego estableceremos los valores de entrada, no la matriz de entrada, porque esta vez estaremos usando un modelo entrenado para predecir los resultados de los valores de entrada. MLPOutput[] nos da el array de salida:

     double weights[], bias[];
     
     int handlew = FileOpen("NNTestScript\\model_w.bin",FILE_READ|FILE_BIN);
     FileReadArray(handlew,weights);
     FileClose(handlew);
     
     int handleb = FileOpen("NNTestScript\\model_b.bin",FILE_READ|FILE_BIN);
     FileReadArray(handleb,bias);
     FileClose(handleb);
     
     double Inputs[]; ArrayCopy(Inputs,XMatrix,0,0,cols); //copy the four first columns from this matrix
     double Output[];
     
     neuralnet = new CNeuralNets(SIGMOID,RELU,cols,hlnodes,outputs);     
     
     neuralnet.FeedForwardMLP(Inputs,Output,weights,bias);
     
     Print("Outputs");
     ArrayPrint(Output);
     
     delete(neuralnet);

Todo esto funciona.

Ahora podremos analizar diferentes tipos de arquitectura y diferentes opciones para ver qué es lo que más nos conviene.

La red neuronal de conexión directa fue el primer tipo de red neuronal desarrollado, y el más sencillo. En esta red, la información solo fluye en una dirección -hacia adelante- desde los nodos de entrada, a través de los nodos ocultos (si los hay) y hacia los nodos de salida. En la red no hay ciclos

El modelo que acabamos de escribir es básico y podría no producir los resultados deseados sin una optimización adecuada (estoy seguro al 100%). Espero que el lector sea creativo y trabaje en ello.

Reflexiones finales

Es importante entender la teoría y todo lo que se encuentra tras las puertas cerradas de las distintas técnicas de aprendizaje automático. No disponemos de paquetes de data science en MQL5, pero al menos tenemos frameworks de Python. No obstante, hay veces que debemos organizar el trabajo en MetaTrader. Sin una comprensión clara de la teoría detrás de este tipo de cosas, resultará complicado que la gente entienda y saque el máximo partido del aprendizaje automático. A medida que avancemos, aumentará la importancia de la teoría y de lo aprendido anteriormente en la serie.

¡Buena suerte a todos!

Repositorio GitHub: https://github.com/MegaJoctan/NeuralNetworks-MQL5

Descubra mi biblioteca para trabajar con matrices y vectores


Bibliografía adicional

Artículos: