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.
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.
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.
Esto nos permitirá probar si la lógica matricial funcionará en todos los escenarios necesarios:
- Cuando la capa anterior (de entrada) tiene menos nodos que la siguiente (de salida)
- Cuando la capa anterior (de entrada) tiene más nodos que la siguiente
- Cuando tenemos el mismo número de nodos en la capa de entrada y de salida
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.
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
- En la primera capa, la matriz de entrada es una matriz de pesos 1x2 = 2x4 y la matriz de salida es 1x4.
- En la segunda capa, la matriz de entrada es una matriz de pesos 1x4 = 4x6 y la matriz de salida es 1x6
- Como consecuencia, la tercera capa incluye una matriz 1x6 de pesos 6x1 y la salida es una matriz 1x1
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
- Vamos a entrenar la red durante x épocas y a encontrar el modelo con menos errores.
- 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.
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:
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.5482Podremos 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
- Neural Networks for Pattern Recognition (Advanced Texts in Econometrics)
- Neural Networks: Tricks of the Trade (Lecture Notes in Computer Science, 7700)
- Deep Learning (Adaptive Computation and Machine Learning series)
Artículos:
- Aprendizaje automático y data science (Parte 01): Regresión lineal
- Aprendizaje automático y data science (Parte 02): Regresión logística
- Aprendizaje automático y data science (Parte 03): Regresión matricial
- Aprendizaje automático y data science (Parte 06): Descenso de gradiente
- Aprendizaje automático y Data Science - Redes neuronales (Parte 01): Análisis de redes neuronales con conexión directa