Red neuronal en la práctica: Retropropagación manual
Introducción
En el artículo anterior Red neuronal en la práctica: Iniciando la cadena, comenzamos a esbozar nuestra primera red de perceptrones de uso más general. En realidad, todavía no se trata exactamente de una red, sino un intento de conectar dos perceptrones en serie. El objetivo era mostrar cómo funciona el mecanismo que permitiría que perceptrones aparentemente aislados trabajasen en conjunto como si fueran una sola entidad. Aunque podemos utilizar un número arbitrario de perceptrones, por el momento sugiero que usemos únicamente dos, ya que un número mayor introduciría ciertas complicaciones innecesarias en esta etapa. Incluso el número de entradas y salidas deberá mantenerse en el mínimo necesario, simplemente porque, cuanto mayor sea la cantidad de datos que tengamos que analizar, más difícil será entender cómo funcionan las cosas.
Puede que hayas notado que los perceptrones intentaban converger hacia una solución. Sin embargo, intentaban hacerlo de forma completamente individual. Esto se debe a que lo que tenemos implementado hasta ahora solo nos permite construir algo similar a lo que se muestra a continuación. 
Imagen 01
El objetivo de este artículo será mostrar cómo podemos hacer que la conexión de dos o más perceptrones funcione, de manera que empiecen a trabajar en conjunto y no como elementos independientes.
¿Es una red de perceptrones o un único perceptrón?
El título de este tema puede parecer un tanto extravagante. Esto se debe a que muchos pueden pensar que no tiene sentido comparar un perceptrón con una red de perceptrones. Hasta cierto punto, esto puede ser cierto. Pero deja de serlo cuando necesitamos pensar en una forma de hacer que varios perceptrones funcionen en conjunto.
Quizá, para entender el motivo de este título, primero tengamos que plantearnos otra pregunta: ¿lo que vemos en la imagen 01 realmente funciona? En cierto modo, sí, pero no de la manera esperada. Esto se debe a que los perceptrones no se están comunicando o, mejor dicho, no están intercambiando datos entre sí, y cada uno toma sus decisiones de forma completamente independiente. Para nuestro propósito, esto no producirá nada realmente útil. Llegados a este punto surge la pregunta: «¿Cómo hacer que los perceptrones trabajen en conjunto?».
Bueno, esta es, de hecho, la gran pregunta. Sin embargo, no tiene una única respuesta. A diferencia de lo que muchos dicen o pueden estar imaginando erróneamente, la respuesta no es necesariamente la retropropagación. Aunque la retropropagación es una solución para gran parte de los problemas, no siempre es lo que implementamos en la práctica. Hay situaciones en las que no se utiliza. Por tanto, todo depende de cada caso concreto y del propósito específico para el que se crea la red de perceptrones en cuestión.
Pero, antes de profundizar en este tema, quiero proponerte algo que ya vimos al principio de esta serie de artículos, cuando expliqué cómo se utiliza la ecuación de una recta para converger hacia mejores valores de las constantes empleadas. Básicamente, esto da lugar a una regresión lineal. A medida que avanzamos, acabamos desarrollando un perceptrón sencillo, pero perfectamente funcional dentro de lo que era el objetivo inicial. Conforme progresemos, en algún momento llegaremos a una red de perceptrones. Sin embargo, antes de que eso ocurra, quiero hacer una pequeña pausa y probar una modificación que te ayudará a entender cómo podemos implementar la comunicación entre los perceptrones. Esto permitirá explicar diversas cuestiones relacionadas con la necesidad de forzar, de algún modo, la convergencia en una red de perceptrones.
Sé que puede parecer muy extraño, sobre todo para quien está intentando entender cómo funcionan las cosas, pero, en la mayoría de los casos, una red de perceptrones no será tan diferente de un perceptrón aislado. Hay casos en los que una red de perceptrones no se parece en nada a lo que se explicará aquí. Pero, en otros, será muy similar a lo que veremos implementado más adelante.
Para entender qué vamos a hacer y, sobre todo, por qué, tendremos que retroceder algunos pasos. Esto se debe a que, tal como está construido actualmente nuestro perceptrón, resulta un tanto difícil explicar con claridad por qué necesitamos otro modelo de implementación y por qué vamos a abandonar el que estamos utilizando ahora. Conviene que esto quede muy claro antes de continuar. Nuestra implementación actual no es, en absoluto, incorrecta. Simplemente resulta algo confusa para explicar con claridad cómo pueden trabajar varios perceptrones en conjunto.
Muy bien, para empezar necesitamos recurrir a un poco de matemáticas. Pero no tienes por qué asustarte: lo que veremos aquí es bastante sencillo, de nivel básico.
Cualquier sistema que utiliza un perceptrón recurre a las matemáticas para encontrar una ecuación que represente un conjunto de datos. Básicamente, la expresión utilizada es la que se muestra a continuación: 
Imagen 02
Aquí tenemos una sola entrada y una sola salida, que es precisamente el valor de < x >. Tanto < a >, que es la pendiente y que, cuando hablamos de perceptrones, suele llamarse peso, como < b >, que es la ordenada en el origen y que aquí conocemos como sesgo, tienen valores fijos. Esto es así tanto si utilizamos un perceptrón como cualquier red de perceptrones. Todo puede resumirse en la expresión de la imagen 02.
Ahora, presta atención. Si el valor de < x > es 10 y el valor de f(x) es 20, puedes ver fácilmente que < a > debería ser igual a dos y < b > debería ser igual a cero. Esto en el caso de una sola entrada. A medida que cambiamos el valor de < x >, la idea de que < a > debería ser dos y < b > debería ser cero se vuelve cada vez más evidente, hasta que finalmente concluimos que la expresión que representa nuestra base de datos sería: 
Imagen 03
Bien, pero ¿adónde quiero llegar con esto? Si has entendido la idea anterior y lo explicado en los artículos anteriores, habrás notado que el perceptrón puede encontrar la expresión que representa la base de datos simplemente comparando el valor de salida con el que proporcionamos como valor esperado. Y puede hacerlo modificando internamente los valores del peso y del sesgo. Pero ¿qué ocurriría si conectáramos dos perceptrones tal como vimos en el artículo anterior y se muestra en la imagen 01? ¿Cuál sería realmente la expresión que debería utilizar cada perceptrón? Pues bien, la expresión sería algo como lo que se muestra a continuación.

Imagen 04
Ahora las cosas empiezan a resultar un poco más interesantes. Observa lo siguiente: f(xout) es el resultado estimado. Por su parte, < xin > es el valor de entrada. Y aquí tenemos dos perceptrones, por así decirlo, conectados en secuencia. Es decir, la entrada de uno es la salida del anterior. Por eso tenemos < a1 > y < b1 >, que representan el perceptrón de entrada. Del mismo modo, < a2 > y < b2 > representan el perceptrón de salida.
Ahora ya no es tan sencillo saber qué valores asignar a las constantes, ¿verdad? Esto se debe a que la variación de una de ellas implica la variación de todo el conjunto, lo que hace que, para la misma base de datos utilizada para generar la imagen 03, ya no podamos usar los valores dos y cero como peso y sesgo, respectivamente.
Y como mencioné hace poco, aquí es donde la cosa se vuelve realmente interesante. Recuerda que este es el modelo más sencillo de todos después del que utiliza un único perceptrón. Otro aspecto es que aquí estamos ignorando cualquier función de activación, ya que estas tienen como objetivo modificar la ecuación de la recta. Pero esto se explicará más adelante.
Ahora presta atención a algo en esta expresión de la imagen 04: observa que se parece mucho a las anteriores. Y las expresiones anteriores corresponderían, en realidad, al modelo de un perceptrón que ya hemos explicado. Así comenzamos a reducir el problema a uno que ya sabemos cómo manejar. Es decir, utilizamos el perceptrón para generar un resultado y, dependiendo del error entre el valor estimado y el valor devuelto por el perceptrón, ajustamos los valores de los pesos y del sesgo. Esto hará que nuestra red se comporte como si fuera un único perceptrón.
Estoy intentando hacerlo todo de la forma más gradual posible. Así se mantiene un nivel didáctico bastante alto y se facilita que todos puedan entenderlo. Llegados a este punto, quienes ya tienen buenos conocimientos de programación empiezan enseguida a imaginar una solución y pueden implementar rápidamente lo necesario, dejando incluso la implementación preparada para modelar una topología más elaborada, en la que no estaríamos restringidos a una única secuencia de perceptrones.
Sin embargo, aquí iremos más despacio, ya que desconozco el nivel de programación de cada uno. Por tanto, suponiendo que haya quedado bastante claro que el modelo de la clase C_Neuron no es, en absoluto, incorrecto y que solo necesita adaptarse para cumplir nuestros objetivos, permitiéndonos controlar cómo se llevará a cabo la retropropagación, podemos empezar a pensar en los cambios necesarios. Pero, para separar adecuadamente las cosas, vamos a iniciar un nuevo tema.Un pequeño cambio, pero con resultados profundos
Aunque los resultados serán interesantes, en términos generales, lo que vamos a empezar haciendo aquí resulta un tanto aburrido. No es realmente algo tan complicado ni requiere un nivel de experiencia extraordinario. Lo que haremos será simplemente modificar el código que vimos en el artículo anterior para aplicar lo explicado en el tema anterior. Y esto sí que carece por completo de emoción, porque es muy sencillo de hacer.
Lo primero será eliminar el código de entrenamiento que estamos utilizando. Pasaremos a utilizar otro código para que el modelo se parezca más al explicado al principio de este artículo. Es decir, no utilizaremos la función Learning de la clase C_Neuron. Crearemos una función local para realizar el mismo trabajo. Pero no te preocupes, lo haremos despacio, paso a paso. Así podrás seguirlo y entender cómo van surgiendo las cosas de forma natural. Entonces, el código del artículo anterior se modificará para quedar como se muestra a continuación.
//+------------------------------------------------------------------+ #property copyright "Daniel Jose" #property description "Experiencing a simple chain of neurons" //+------------------------------------------------------------------+ #define def_NO_MSG //+------------------------------------------------------------------+ #include <Neural Network\C_Neuron.mqh> //+------------------------------------------------------------------+ //Training expression: f(x) = (w0 * 2) //+------------------------------------------------------------------+ double Train[] { 10, 20 }; //+------------------------------------------------------------------+ #define nColumns 2 #define nLines Train.Size() / nColumns //+------------------------------------------------------------------+ void SimpleChain(const uchar nNeurons) { C_Neuron *neuron[]; double tmp[], in, out; if ((!nNeurons) || (Train.Size() > 2)) return; ArrayResize(neuron, nNeurons); for (uchar c = 0; c < nNeurons; c++) neuron[c] = new C_Neuron(nColumns - 1, C_Neuron::Identity); ArrayResize(tmp, Train.Size()); in = out = Train[0]; for (uchar c = 0; c < nNeurons; c++) { tmp[0] = (c > 0 ? out : in); PrintFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]); out = (*neuron[c]).Perceptron(tmp); } PrintFormat("Final exit: %.8f || Expected: %.8f", out, Train[1]); for (uchar c = 0; c < nNeurons; c++) delete neuron[c]; ArrayFree(tmp); ArrayFree(neuron); } //+------------------------------------------------------------------+ void OnStart() { Print("************************************"); Print("A simple chain of neurons..."); Print("************************************"); SimpleChain(2); } //+------------------------------------------------------------------+
Código 01
Claro que, si intentas ejecutarlo, no obtendrás ningún resultado espectacular. Solo verás lo que se muestra en la siguiente imagen, es decir, lo mismo que antes.

Imagen 05
Ahora quiero que pienses en lo siguiente: si pedimos a los perceptrones que ajusten sus pesos y sesgos, ¿qué ocurrirá? Obviamente, obtendremos un valor diferente a la salida de cada uno de los perceptrones. Pero pensemos un poco más a fondo. ¿Y si, en lugar de modificar los valores de forma independiente, hacemos que ambos perceptrones cambien los pesos y los sesgos al mismo tiempo y en la misma magnitud? ¿Qué ocurriría en ese caso? Hum, no sé responder a eso. Pues bien, para hacerlo necesitamos añadir un nuevo procedimiento a la clase C_Neuron, ya que, tal como está ahora, todavía no podemos hacer este tipo de cosas.
Este nuevo procedimiento puede verse en el siguiente fragmento.
//+------------------------------------------------------------------+ inline void BackPropagation(const double errBias, const double errWeight) { m_Infos.Bias -= errBias; for (uint c = 0, m = m_Infos.Weight.Size(); c < m; c++) m_Infos.Weight[c] -= errWeight; } //+------------------------------------------------------------------+
Fragmento 01
El nombre del procedimiento es bastante sugerente, pero, en realidad, aquí le estamos indicando al perceptrón cuánto deberá modificar los pesos y el sesgo. Pero ¿cómo accederemos a esto? Pues bien, volviendo al código principal, podemos ver cómo deberá realizarse este ajuste. Como no necesitamos modificar demasiado el código, vamos a centrarnos únicamente en el fragmento principal, que se muestra a continuación.
17. //+------------------------------------------------------------------+ 18. void SimpleChain(const uchar nNeurons, const double errBias, const double errWeight) 19. { 20. C_Neuron *neuron[]; 21. double tmp[], in, out; 22. 23. if ((!nNeurons) || (Train.Size() > 2)) 24. return; 25. 26. ArrayResize(neuron, nNeurons); 27. for (uchar c = 0; c < nNeurons; c++) 28. neuron[c] = new C_Neuron(nColumns - 1, C_Neuron::Identity); 29. 30. ArrayResize(tmp, Train.Size()); 31. in = out = Train[0]; 32. for (uchar count = 1; count < 3; count++) 33. { 34. PrintFormat("====== Interaction #%02d ======", count); 35. for (uchar c = 0; c < nNeurons; c++) 36. { 37. tmp[0] = (c > 0 ? out : in); 38. PrintFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]); 39. out = (*neuron[c]).Perceptron(tmp); 40. } 41. PrintFormat("Final exit: %.8f || Expected: %.8f", out, Train[1]); 42. for (uchar c = 0; c < nNeurons; c++) 43. (*neuron[c]).BackPropagation(errBias, errWeight); 44. } 45. 46. for (uchar c = 0; c < nNeurons; c++) 47. delete neuron[c]; 48. 49. ArrayFree(tmp); 50. ArrayFree(neuron); 51. } 52. //+------------------------------------------------------------------+
Fragmento 02
Muy bien, la idea es forzar a ambos perceptrones a ajustar al mismo tiempo los valores del sesgo y del peso. Esto se conseguirá pasando los valores como errBias y errWeight. Como realizaremos solo dos iteraciones, el objetivo no será llegar a un valor perfecto, sino simplemente comprobar una hipótesis. Observa que, en la primera iteración, tenemos los valores generados de forma pseudoaleatoria por el perceptrón. Sin embargo, en la línea cuarenta y dos entramos en un bucle cuyo objetivo es forzar a todos los perceptrones a ajustar sus valores de forma idéntica. Es decir, todos serán ajustados mediante el nuevo procedimiento que añadimos a la clase C_Neuron, independientemente del error de cada perceptrón.
Con esto, en la segunda iteración tendremos un valor diferente devuelto por el perceptrón en la línea treinta y nueve. Al observar este valor, podrás ver cómo los perceptrones intentan ajustar adecuadamente los pesos y el sesgo. A continuación podemos observar un resultado típico de la ejecución del código. 
Imagen 06
Nuestro objetivo principal es hacer que los valores mostrados en la región marcada en verde sean lo más próximos posible entre sí. Observa que necesitamos ajustar el valor del sesgo, ya que, de acuerdo con los valores de entrenamiento, tenemos: f(x) = 2 * x + 0. Es decir, el sesgo deberá estar lo más cerca posible de cero. Por su parte, el valor del peso deberá estar lo más cerca posible de dos. Pero ¿cómo hice esto en la imagen 06? Pues bien, si te fijas, verás que los valores del resultado están invertidos, además de encontrarse muy cerca de los valores ideales. Esto indica que los valores deben invertirse. Así, invirtiéndolos y ajustándolos un poco, con paciencia y calma, podemos llegar al resultado que se muestra en la siguiente imagen.

Imagen 07
Por tanto, no necesitamos hacer gran cosa. Lo que necesitamos es que el perceptrón haga automáticamente lo mismo que acabamos de hacer de forma manual, sin nuestra intervención. Pero, antes, vamos a realizar un último cambio en el código para hacer las cosas todavía más interesantes. La idea es evitar tener que compilar el código cada vez que probemos nuevos valores. De esta forma, podremos analizar algunas situaciones bastante inusuales y ver qué resultados pueden generar según las decisiones que tomemos. El objetivo es disponer de los siguientes datos, que podrán ajustarse de forma interactiva, como se muestra en la siguiente imagen.

Imagen 08
Lo interesante de este enfoque está precisamente en la posibilidad de interactuar con él. Es cierto que podemos mejorar todavía más esta interacción. Sin embargo, dejo que pienses en formas más elaboradas de gestionar esta interacción. En cualquier caso, este enfoque sencillo es mejor que estar modificando y recompilando el código continuamente. Así, a continuación puede verse un ejemplo de ejecución. 
Imagen 09
En este caso, estamos utilizando la función de activación identidad y, al mismo tiempo, dos perceptrones. Y, sin cambiar nada, podemos realizar otra prueba y obtener como resultado lo que se muestra en la siguiente imagen.

Imagen 10
Ahora estamos utilizando cinco perceptrones conectados en cascada. Esta vez, sin embargo, usamos la función eLU como función de activación. Es importante observar que la función elegida se aplicará a todos los perceptrones. Más adelante veremos cómo hacer que la función de activación de cada perceptrón sea diferente de la de los demás. La explicación detallada quedará para otro momento.
Ahora bien, puede que pienses que, para alcanzar este nivel de interacción, fue necesario modificar mucho el código. Sin embargo, observa lo que se muestra a continuación y responde: ¿realmente fue necesario modificar el código tanto como imaginabas?
//+------------------------------------------------------------------+ #property copyright "Daniel Jose" #property script_show_inputs #property description "Experiencing a simple chain of neurons" //+------------------------------------------------------------------+ #define def_NO_MSG //+------------------------------------------------------------------+ #include <Neural Network\C_Neuron.mqh> //+------------------------------------------------------------------+ //Training expression: f(x) = (w0 * 2) //+------------------------------------------------------------------+ double Train[] { 10, 20 }; //+------------------------------------------------------------------+ #define nColumns 2 #define nLines Train.Size() / nColumns //+------------------------------------------------------------------+ input C_Neuron::eFnActivate user00 = C_Neuron::Identity; //Activation function input uchar user01 = 2; //Number of neurons input double user02 = 1.0; //Bias Error input double user03 = 1.0; //Weight Error //+------------------------------------------------------------------+ void SimpleChain(const uchar nNeurons, const double errBias, const double errWeight) { C_Neuron *neuron[]; double tmp[], in, out; if ((!nNeurons) || (Train.Size() > 2)) return; ArrayResize(neuron, nNeurons); for (uchar c = 0; c < nNeurons; c++) neuron[c] = new C_Neuron(nColumns - 1, user00); ArrayResize(tmp, Train.Size()); in = out = Train[0]; for (uchar count = 1; count < 3; count++) { PrintFormat("====== Interaction #%02d ======", count); for (uchar c = 0; c < nNeurons; c++) { tmp[0] = (c > 0 ? out : in); PrintFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]); out = (*neuron[c]).Perceptron(tmp); } PrintFormat("Final exit: %.8f || Expected: %.8f", out, Train[1]); for (uchar c = 0; c < nNeurons; c++) (*neuron[c]).BackPropagation(errBias, errWeight); } for (uchar c = 0; c < nNeurons; c++) delete neuron[c]; ArrayFree(tmp); ArrayFree(neuron); } //+------------------------------------------------------------------+ void OnStart() { Print("************************************"); Print("A simple chain of neurons..."); Print("************************************"); Print("Parameters:"); Print("Activation function: ", EnumToString(user00)); Print("Bias Error: ", user02); Print("Weight Error:", user03); Print("************************************"); SimpleChain(user01, user02, user03); } //+------------------------------------------------------------------+
Código 02
Perfecto. Pero, antes de terminar el artículo, quiero proponer un último cambio que aumentará todavía más el nivel de interacción. Pretendo profundizar un poco más en este tema, ya que cuanto mejor entiendas lo que está ocurriendo aquí, más sencillo será comprender lo que veremos después. Hagamos lo siguiente: abriremos un nuevo tema para separar adecuadamente los asuntos.Cuanto más interactivo, mejor
Aunque la implementación realizada mediante el script resulta, hasta cierto punto, agradable, tarde o temprano acaba volviéndose algo monótona. Podemos hacer que resulte un poco más cómoda de utilizar en la práctica. Para ello, añadiremos una nueva forma de interacción en la que podremos utilizar las teclas de dirección para ajustar los valores. El código en sí no es complicado; en realidad, es una modificación de un código más antiguo que ya vimos en esta serie. Sin embargo, esta vez utilizaremos la idea de una forma un poco diferente, precisamente para que el uso de la aplicación resulte menos monótono.
El código estará compuesto por dos partes. Una estará en un archivo de cabecera y la otra en un indicador. La parte correspondiente al archivo de cabecera, que aparece en el siguiente fragmento, estará incluida en el archivo C_SimpleChain, que podrás consultar íntegramente en el archivo adjunto. Sin embargo, necesitamos explicar el siguiente fragmento.
33. //+------------------------------------------------------------------+ 34. bool Interaction(const double errBias, const double errWeight, string &arr[]) 35. { 36. double tmp[], in, out; 37. uchar cArr = 0; 38. 39. if ((m_Info.nNeurons == 0) || (m_Info.nColumns == 0)) 40. return false; 41. 42. ArrayResize(neuron, m_Info.nNeurons); 43. for (uchar c = 0; c < m_Info.nNeurons; c++) 44. neuron[c] = new C_Neuron(m_Info.nColumns - 1, m_Info.fn); 45. ArrayResize(arr, (m_Info.nNeurons + 2) * 2); 46. ArrayResize(tmp, m_Info.Train.Size()); 47. in = out = m_Info.Train[0]; 48. for (uchar count = 1; count < 3; count++) 49. { 50. arr[cArr++] = StringFormat("====== Interaction #%02d ======", count); 51. for (uchar c = 0; c < m_Info.nNeurons; c++) 52. { 53. tmp[0] = (c > 0 ? out : in); 54. arr[cArr++] = StringFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]); 55. out = (*neuron[c]).Perceptron(tmp); 56. } 57. arr[cArr] = StringFormat("Final exit: %.8f || Expected: %.8f", out, m_Info.Train[1]); 58. for (uchar c = 0; c < m_Info.nNeurons; c++) 59. (*neuron[c]).BackPropagation(errBias, errWeight); 60. } 61. 62. for (uchar c = 0; c < m_Info.nNeurons; c++) 63. delete neuron[c]; 64. ArrayFree(neuron); 65. 66. return true; 67. } 68. //+------------------------------------------------------------------+
Fragmento 03
Observa que, en la línea treinta y cuatro, tenemos un array de retorno de tipo string. Esto nos permitirá dirigir posteriormente los datos recopilados a un lugar más adecuado. Mi recomendación es que no intentes modificar este código antes de entender realmente cómo funciona. No estoy realizando comprobaciones de rango para garantizar que no se intente acceder a una región no válida de la memoria. Por tanto, cualquier situación fuera de lo normal hará que el código falle, ya que es muy sensible a pequeños cambios en su estructura. Sin embargo, quiero llamar tu atención sobre el bucle de la línea cuarenta y ocho. Aquí hacemos lo mismo que se hacía en el script del tema anterior. No obstante, si te fijas en la línea cuarenta y tres, en cada llamada de interacción estaremos reconstruyendo la red de perceptrones.
Presta atención a este detalle. La red de perceptrones creada solo existirá dentro de esta función. Esto se debe a que utilizaremos esta red en un indicador. Y, como el indicador no se volverá a colocar en el gráfico con cada nueva interacción, si la red no se destruyera, no podríamos forzar el tipo de interacción que queremos. Observa que, al final, en la línea cincuenta y ocho, hacemos lo que podríamos llamar retropropagación, del mismo modo que se hacía en el script. Pero, como aquí la interacción será constante, todo puede resultar un tanto confuso si no entiendes que la red solo existe dentro del procedimiento del fragmento 03. En la línea sesenta y dos entramos en un bucle que destruirá la red construida en este procedimiento.
Tal vez este sistema merezca un artículo dedicado exclusivamente a explicarlo. Pero, como gran parte del código ya se explicó debidamente al principio de este artículo, no veo la necesidad de dedicar otro artículo únicamente a explicar el código completo. Esto no significa que no debamos explicar con un poco más de detalle el código del indicador, ya que contiene algunas partes que pueden generar dudas. Sin embargo, esas dudas no surgirán en quienes ya estén familiarizados con la programación en MQL5. Por esta razón, la explicación que daremos aquí está dirigida a quienes están empezando a programar y quieren entender cómo funciona el indicador.
A continuación se muestra el código completo. Así que, si no estás seguro de cómo funciona, presta atención a la explicación.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property indicator_chart_window 004. #property indicator_plots 0 005. //+------------------------------------------------------------------+ 006. #define def_NO_MSG 007. //+------------------------------------------------------------------+ 008. #include <Canvas\Canvas.mqh> 009. #include <Neural Network\C_SimplesChain.mqh> 010. //+------------------------------------------------------------------+ 011. input C_Neuron::eFnActivate user00 = C_Neuron::Identity; //Activation function 012. input uchar user01 = 2; //Number of neurons 013. input double user02 = 1e-3; //Learning Rate 014. //+------------------------------------------------------------------+ 015. CCanvas *canvas; 016. C_SimpleChain *gl_Chain; 017. //+------------------------------------------------------------------+ 018. double Train[] { 019. 10, 20 020. }; 021. //+------------------------------------------------------------------+ 022. #define nColumns 2 023. #define nLines Train.Size() / nColumns 024. //+------------------------------------------------------------------+ 025. struct st_00 026. { 027. int x, 028. y, 029. sizeLine; 030. uint height; 031. double errBias, errWeight; 032. }global; 033. //+------------------------------------------------------------------+ 034. void NewPoint(const char lr, const char ud) 035. { 036. #define macroScale(a, b, c) MathAbs(MathAbs(a/b) > c ? c/a : 1/b) 037. 038. double sx, sy; 039. string SzArr[]; 040. 041. (*canvas).Erase(ColorToARGB(clrWhite, 255)); 042. (*canvas).LineVertical(global.x, global.y - global.sizeLine, global.y + global.sizeLine, ColorToARGB(clrRoyalBlue, 255)); 043. (*canvas).LineHorizontal(global.x - global.sizeLine, global.x + global.sizeLine, global.y, ColorToARGB(clrRoyalBlue, 255)); 044. 045. global.errBias += (user02 * lr); 046. global.errWeight += (user02 * ud); 047. 048. if (!(*gl_Chain).Interaction(global.errBias, global.errWeight, SzArr)) return; 049. 050. sx = macroScale(global.errBias, user02, global.sizeLine); 051. sy = macroScale(global.errWeight, user02, global.sizeLine); 052. 053. (*canvas).FillCircle(global.x + (int)(global.errBias * sx), global.y - (int)(global.errWeight * sy), 5, ColorToARGB(clrRed, 255)); 054. (*canvas).FillCircle(global.x + (int)(global.errBias * sx), global.y - (int)(global.errWeight * sy), 3, ColorToARGB(clrBlack, 255)); 055. 056. (*canvas).TextOut(5, global.height * 0, StringFormat("Activation function: %s", EnumToString(user00)), ColorToARGB(clrDarkSlateGray)); 057. (*canvas).TextOut(5, global.height * 2, StringFormat("Bias Error : %.8f", global.errBias), ColorToARGB(clrMediumBlue)); 058. (*canvas).TextOut(5, global.height * 3, StringFormat("Weight Error : %.8f", global.errWeight), ColorToARGB(clrMediumBlue)); 059. for(uint c = 0, l = 5, m = SzArr.Size(), t = m - 2; c < m; c++, l+=(c == t ? 2 : 1)) 060. (*canvas).TextOut(5, global.height * l, SzArr[c], ColorToARGB(c == t ? clrPurple : clrBlack)); 061. 062. (*canvas).Update(true); 063. 064. ArrayFree(SzArr); 065. 066. #undef macroScale 067. } 068. //+------------------------------------------------------------------+ 069. int OnInit() 070. { 071. uint w; 072. 073. ZeroMemory(global); 074. ChartSetInteger(0, CHART_SHOW_DATE_SCALE, false); 075. ChartSetInteger(0, CHART_SHOW_PRICE_SCALE, false); 076. TextGetSize("W", w, global.height); 077. 078. canvas = NULL; 079. 080. gl_Chain = new C_SimpleChain(user00, (uchar)(user01 < 1 ? 1 : user01), Train, nColumns); 081. 082. return INIT_SUCCEEDED; 083. } 084. //+------------------------------------------------------------------+ 085. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 086. { 087. return rates_total; 088. } 089. //+------------------------------------------------------------------+ 090. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 091. { 092. switch (id) 093. { 094. case CHARTEVENT_CHART_CHANGE: 095. if (canvas != NULL) 096. { 097. (*canvas).Destroy(); 098. delete canvas; 099. } 100. 101. canvas = new CCanvas; 102. 103. global.x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0); 104. global.y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0); 105. 106. (*canvas).CreateBitmapLabel("BL", 0, 0, global.x, global.y, COLOR_FORMAT_ARGB_NORMALIZE); 107. global.x /= 2; 108. global.y /= 2; 109. global.sizeLine = MathMin(global.y, global.x) - 10; 110. 111. NewPoint(0, 0); 112. break; 113. case CHARTEVENT_KEYDOWN: 114. if (TerminalInfoInteger(TERMINAL_KEYSTATE_LEFT)) 115. NewPoint(-1, 0); 116. if (TerminalInfoInteger(TERMINAL_KEYSTATE_RIGHT)) 117. NewPoint(1, 0); 118. if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) 119. NewPoint(0, 1); 120. if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN)) 121. NewPoint(0, -1); 122. break; 123. } 124. } 125. //+------------------------------------------------------------------+ 126. void OnDeinit(const int reason) 127. { 128. delete gl_Chain; 129. (*canvas).Destroy(); 130. delete canvas; 131. ChartSetInteger(0, CHART_SHOW_DATE_SCALE, true); 132. ChartSetInteger(0, CHART_SHOW_PRICE_SCALE, true); 133. } 134. //+------------------------------------------------------------------+
Código 03
Este código, al ejecutarse, producirá lo que se muestra en el siguiente vídeo.
Vídeo 01
Observa cómo la interacción aquí es mucho mayor que antes, cuando utilizábamos el script. Pero este indicador no deberá utilizarse por sí solo. Debes utilizarlo como complemento para entender lo que ocurre internamente en la red de perceptrones.
Pues bien, en la línea seis tenemos una definición que ya se explicó en otro artículo de esta serie. En la línea dieciocho tenemos el array de entrenamiento. Un detalle: puedes cambiar los valores de este array, pero NO CAMBIES LA CANTIDAD DE VALORES. De lo contrario, tendrás problemas con este indicador. Esto se debe a que no está implementado para permitir más de una entrada en la red que se construirá. Del mismo modo, tampoco está pensado para tener más de una salida.
Básicamente, tanto si eres usuario como programador principiante, solo puedes modificar los valores que aparecen al probar este indicador en MetaTrader 5. Mira el vídeo para entenderlo mejor. Aparte de los detalles mostrados en el vídeo, y volviendo al código, en la línea ciento trece tenemos la detección de algunas teclas. Estas indican cuánto deben ajustarse el sesgo y los pesos. También tenemos un evento, en la línea noventa y cuatro, que ayuda a adaptar los elementos al tamaño del gráfico. Es algo sencillo cuyo funcionamiento creo que podrás entender incluso si estás empezando.
Ahora observa la línea ochenta. En ella creamos la cadena de perceptrones, formando así una pequeña red sencilla. En este punto hacemos una comprobación para evitar que el usuario introduzca un valor negativo o incluso igual a cero, ya que una red con esos valores no tendría sentido. Por tanto, si esto ocurre, forzamos la red para que se construya con al menos un perceptrón.
Ahora podemos ir al núcleo de este indicador, que es precisamente el procedimiento de la línea treinta y cuatro. Es un procedimiento bastante directo, sin rodeos. En la línea cuarenta y uno limpiamos el bitmap de la imagen que se mostrará. Las líneas cuarenta y dos y cuarenta y tres crean los ejes cartesianos. Las líneas cuarenta y cinco y cuarenta y seis ajustan los valores de error, es decir, la modificación del sesgo y del peso. Observa que el paso viene determinado por el valor que puedes ajustar al añadir el indicador al gráfico. Esta sería la denominada tasa de aprendizaje. En la línea cuarenta y ocho ejecutamos un ciclo de dos interacciones con la red de perceptrones.
La primera interacción muestra los valores iniciales. En la segunda, vemos los valores ya modificados por el ajuste que estamos realizando. Esto se explicó a lo largo del artículo. Una vez hecho esto, las siguientes líneas, de la cincuenta a la sesenta, crearán todos los datos que se presentarán. En la línea sesenta y dos mostramos los datos en el gráfico del símbolo. En la línea sesenta y cuatro liberamos la memoria asignada por la clase C_SimpleChain.
Gran parte de este código es bastante directa. Pero observa la línea cincuenta y nueve, donde tenemos un bucle for y obtenemos algunos datos que C_SimpleChain colocó en el array de tipo string. En este punto podemos presentar esos datos al usuario. De esta forma, la clase C_SimpleChain no necesitará saber qué haremos con ellos. Podemos enviarlos a cualquier destino o incluso convertirlos para obtener otros datos que puedan utilizarse más adelante.
Consideraciones finales
Este artículo tuvo como objetivo presentar, de forma muy breve y sencilla, un mecanismo que permitiera a un conjunto discreto de perceptrones trabajar en conjunto. A grandes rasgos, este mecanismo podría llamarse retropropagación. Sin embargo, ese no es realmente su objetivo; en realidad, se trata únicamente de una forma interactiva de ajustar de una sola vez los pesos y los sesgos de todos los perceptrones.
Lo que realmente debe entenderse aquí es que un simple cambio en el código nos permitió dejar atrás lo que teníamos en la imagen 01 y pasar a algo similar a lo que se muestra en la siguiente imagen. 
Imagen 11
Este sencillo cambio en la estructura, que a primera vista puede parecer algo trivial, permite ver que podemos implementar cualquier funcionalidad siempre que sepamos lo que estamos haciendo. En el próximo artículo veremos cómo el mecanismo desarrollado aquí puede ayudarnos a entender mejor cómo funciona realmente la retropropagación.| Archivo MQ5 | Descripción |
|---|---|
| Scripts\Example 01 | Demostración básica |
| Indicators\Example 01 | Demostración básica |
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/13913
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Del nivel básico al intermedio: Archivo de plantilla (II)
Oscilador Parafrac: Combinación del SAR parabólico y el indicador fractal
Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 12): Construcción de clasificadores lineales mediante factorización matricial
Red neuronal en la práctica: Una cuestión de escala
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso