English Русский 中文 Deutsch 日本語 Português
preview
Redes neuronales: así de sencillo (Parte 27): Aprendizaje Q profundo (DQN)

Redes neuronales: así de sencillo (Parte 27): Aprendizaje Q profundo (DQN)

MetaTrader 5Sistemas comerciales | 30 noviembre 2022, 13:53
558 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Contenido

Introducción

En un artículo anterior, comenzamos a explorar los métodos de aprendizaje por refuerzo y construimos el primer modelo entrenado con entropía cruzada. En este artículo, proseguiremos nuestro estudio de los métodos de aprendizaje por refuerzo. Hoy hablaremos el método de aprendizaje Q profundo o deep Q-learning. Fue gracias al uso del aprendizaje Q profundo que el equipo de DeepMind pudo crear un modelo capaz de jugar con éxito a 7 videojuegos de ordenador de Atari en 2013. Sorprendentemente, el mismo modelo fue entrenado en los 7 juegos sin ningún cambio en la arquitectura o en los hiperparámetros. En este caso, además, el modelo pudo mejorar los resultados anteriores en 6 de las partidas analizadas. Además, el modelo obtuvo mejores resultados que un humano en 3 de los juegos. Podría decirse que con este trabajo se inició una nueva fase en el desarrollo del aprendizaje por refuerzo. Hoy nos familiarizaremos con este método e intentaremos utilizarlo para resolver nuestros problemas.


1. El concepto de función Q

En primer lugar, repasaremos un poco lo aprendido en el último artículo. En el aprendizaje por refuerzo, construimos un proceso de interacción entre el agente y el entorno. El agente analiza el estado actual del entorno y efectúa una acción que provoca un cambio en el estado del entorno. En respuesta a la acción, el entorno retorna una recompensa al agente. El agente desconoce la naturaleza de la formación de la recompensa, pero su objetivo es obtener la mayor recompensa total posible por la sesión analizada.

Obsérvese que el agente no está siendo recompensado por la acción, sino por pasar de un estado a otro. No obstante, realizar una determinada acción en una situación similar no garantiza la transición al mismo estado. La realización de una acción solo da cierta probabilidad de pasar al estado esperado. Las probabilidades y dependencias de los estados, acciones y transiciones son desconocidas para el agente, que tiene que aprenderlas a partir del proceso de interacción con el entorno. 

En esencia, el aprendizaje por refuerzo se basa en la suposición de que existe alguna relación entre el estado actual, la acción realizada y la recompensa. Hablando en términos matemáticos, existe una función Qque, según el estado s y la acción a, retornará una recompensa r. Y se denota mediante Q(s|a). Esta función se llama función de utilidad de la acción.

En efecto, el agente no conoce esta función, pero si existe, podemos aproximarnos a ella mientras interactuamos con el entorno repitiendo la acción un número infinito de veces.

Obviamente, en la vida real, no podemos repetir estados y acciones un número infinito de veces, pero, repitiéndolos un número suficiente de veces, podemos aproximar la función con un error aceptable. La forma de expresión de la función Q puede ser distinta. En un artículo anterior, creamos una tabla de dependencias de estado, acción y recompensa media para determinar la utilidad de cada acción, pero otras formas de expresión de la función Q resultan bastante aceptables y pueden dar incluso mejores resultados. Pueden ser árboles de decisión, redes neuronales, etcétera.

Nótese que la función Q aproximada por el agente no predice la recompensa obtenida, solo retorna la recompensa esperada basándose en la experiencia transmitida del agente al interactuar con el entorno.


2. Aprendizaje Q profundo

Probablemente ya habrá adivinado que el aprendizaje Q profundo consiste en utilizar una red neuronal para aproximar una función Q. ¿En qué consiste la ventaja de este enfoque? Recordemos la implementación del método de entropía cruzada tabular en el último artículo: en él destacamos que la aplicación del método tabular supone un número finito de estados y acciones posibles. Obviamente, hemos limitado el número de estados posibles al clusterizar los datos de origen. ¿Pero es esto tan bueno? ¿La clusterización nos ofrecerá siempre mejores resultados? Al mismo tiempo, el uso de una red neuronal no pone un límite al número de estados posibles que tenemos delante, y en el caso de las tareas comerciales, parece que esto supone una gran ventaja.

Aquí diríamos que el enfoque obvio consiste en tomar la tabla del artículo anterior y sustituirla por una red neuronal, pero, por desgracia, no resulta tan sencillo. En la práctica, este enfoque no es tan bueno como parece a primera vista. Para aplicarlo, deberemos añadir algunas heurísticas.

En primer lugar, veremos el objetivo del entrenamiento de nuestro agente. En general, consiste en maximizar la recompensa total. Echemos un vistazo a la figura: El agente se moverá de la casilla Start a la casilla Finish, y obtendrá una única recompensa cada vez que entre en la casilla Finish. En todos los demás estados, la recompensa será cero.

Factor de descuento

La ilustración muestra dos rutas: para nosotros, resulta evidente que la ruta naranja es más corta y preferible. Sin embargo, desde el punto de vista de la maximización de la recompensa, son equivalentes.

Del mismo modo, en el trading, es mejor obtener beneficios de inmediato, que invertir el dinero ahora y obtener un rendimiento en un futuro lejano. Obviamente, aquí deberemos tener en cuenta el valor del dinero: el factor de descuento, la inflación y otra serie de atributos. Aquí hacemos lo mismo. Para resolver el problema, introduciremos el factor de descuento ɣ lo cual reducirá el valor de las futuras recompensas.

Recompensa total

El factor de descuento ɣ se selecciona en un rango de 0 a 1. Si el factor de descuento es igual a 1, no habrá descuento, mientras que con un factor de descuento de 0, no se tendrán en cuenta las recompensas futuras. En la práctica, el factor de descuento se suele tomar próximo a 1.

Sin embargo, aquí hay otro problema: lo que parece bonito sobre el papel no siempre se puede realizar en la práctica. Podemos calcular fácilmente las futuras recompensas cuando tenemos delante un mapa completo de transiciones y recompensas. Entre ellas, podemos elegir la mejor ruta con la máxima recompensa al final, pero en la resolución de problemas prácticos, no sabemos cuál será el siguiente estado que alcanzaremos al realizar una acción, ni qué recompensa se obtendrá. Y eso es solo el siguiente paso, ¿qué decir de toda la ruta hasta el final de la sesión? No podemos ver el futuro. Para obtener otra recompensa, el agente deberá realizar una acción, y solo después de la transición al nuevo estado, el entorno retornará la recompensa. Al hacerlo, no habrá vuelta atrás. No podremos volver a un estado anterior y realizar una acción diferente para elegir una mejor más adelante.

Por eso recurrimos a los métodos de programación dinámica, y, en particular, al método de optimización de Bellman. Esta establece que para elegir la mejor estrategia, deberemos seleccionar la mejor acción en cada paso. Es decir, seleccionando la acción con la máxima recompensa en cada paso, obtendremos la máxima recompensa total por sesión. La fórmula matemática para actualizar la función de utilidad de la acción se muestra a continuación.

Optimización de Bellman

Veamos la fórmula presentada. ¿No le recuerda a la fórmula de actualización de los pesos de un descenso de gradiente estocástico? De hecho, aquí vemos que para actualizar el valor de la función de utilidad de la acción necesitamos el valor anterior de la función más alguna desviación multiplicada por el coeficiente de aprendizaje.

Sin embargo, en la función presentada también podemos ver que para determinar el valor de la función en el punto de tiempo t necesitamos el valor de la función de utilidad de la acción en el siguiente paso de tiempo en el punto t+1. En otras palabras, encontrándonos en el estado st, realizaremos la acción at, y después de pasar al estado st+1, obtendremos la recompensa rt+1. Para actualizar el valor de la función de utilidad de la acción, deberemos añadir el máximo de la función de utilidad de la acción en el siguiente paso a la recompensa recibida, es decir, la máxima recompensa esperada que podemos obtener en el siguiente paso. Obviamente, nuestro agente no puede mirar hacia el futuro y determinar la recompensa futura, pero puede utilizar su función aproximada y, encontrándose en el estadost+1, calcular el valor de la función para todas las acciones posibles desde ese estado y tomar el máximo de los valores obtenidos. En efecto, durante el aprendizaje, sus valores estarán lejos de ser verdaderos al principio, pero eso es mejor que nada, y a medida que el agente aprenda, el error de predicción disminuirá.


2.1. Reproduciendo la experiencia

El descenso de gradiente estocástico es bueno porque nos permite actualizar los valores de la función basándonos en pequeños valores de muestra de la población general. En esencia, permitiremos a nuestro agente actualizar los valores de la función de utilidad de la acción en cada paso de la sesión, pero en el aprendizaje supervisado, utilizaremos una muestra de entrenamiento cuyos estados son independientes entre sí. Para reforzar esta característica, barajaremos la población general cada vez antes de seleccionar un nuevo paquete de datos de entrenamiento.

No obstante, en el caso del aprendizaje supervisado, al moverse en el tiempo en nuestro entorno, el agente entra en un nuevo estado cada vez que se realiza una acción estrechamente relacionada con la anterior. Mire a su alrededor. Tanto si está caminando como si está sentado a una mesa haciendo algo, el entorno en su campo de visión no cambia drásticamente. Lo único que cambiará es la pequeña parte que se ve afectada por su acción. De la misma forma, los estados del entorno analizado no cambiarán sustancialmente cuando el agente actúe, y así los estados sucesivos poseerán una correlación bastante grande. Nuestro agente observará la autocorrelación de dichos estados.

La dificultad aquí reside en que incluso el uso de un pequeño coeficiente de aprendizaje no salvará a nuestro agente de ajustar la función de utilidad de la acción al estado actual, en detrimento de la memoria de la experiencia pasada.

En el aprendizaje supervisado, el uso de estados independientes después de un número bastante elevado de iteraciones permite promediar los valores de los pesos del modelo entrenado. En cambio, en el aprendizaje por refuerzo, cuando entrenamos un modelo sobre estados relacionados y, en la práctica, invariables, el modelo se reentrena sobre el estado actual.

Como en cualquier serie temporal, la correlación de los estados disminuirá a medida que aumente el tiempo entre ellos. En consecuencia, para resolver este problema, necesitaremos usar estados dispersos en la línea temporal al entrenar nuestro modelo de agente. Podemos hacerlo fácilmente con datos históricos, pero al pasar por el entorno, nuestro agente no poseerá esa memoria, solo verá el estado actual y no podrá saltar de un estado a otro.

En ese caso, ¿por qué no organizar una memoria para el agente? Mire, para actualizar el valor de la función de utilidad de la acción necesitaremos el siguiente conjunto de datos:

Estado -> Acción -> Recompensa -> Estado

Así que vamos a hacer que nuestro agente almacene el conjunto de datos necesarios en algún tipo de búfer mientras recorre los estados del entorno. El tamaño del búfer será un hiperparámetro determinado por el arquitecto del modelo. Cuando el búfer esté lleno, los datos más antiguos serán sustituidos por los nuevos datos recibidos. En este caso, no utilizaremos el estado actual para entrenar el modelo, sino que seleccionaremos aleatoriamente algunos estados del búfer de memoria del agente. De esta forma, minimizaremos la correlación entre los estados individuales y aumentaremos la capacidad del modelo para generalizar los datos estudiados.


2.2. Usando la Target Net

Otra cosa que deberemos considerar al entrenar una función de utilidad de la acción es el valor máximo de esa función en el siguiente paso maxQ(st+1|at+1). En primer lugar, deberemos tener claro que se trata de un "valor del futuro". Sí, tomaremos el valor previsto basado en nuestra función de utilidad de acción aproximada, pero hallándonos en un punto en el tiempo t, no podremos cambiar el valor del estado de tiempo t+1. Sin embargo, cada vez que actualicemos el valor de la función, actualizaremos los pesos de nuestro modelo y, por lo tanto, cambiaremos el siguiente valor predicho.

Además, estamos entrenando a nuestro agente para obtener la máxima recompensa, es decir, en cada iteración de la actualización del modelo maximizaremos el valor esperado, y la utilización del valor previsto maximizará recursivamente el valor actualizado. Esto maximizará los valores de nuestra función de utilidad de la acción en la progresión, provocando una sobreestimación de los valores de nuestra función y un aumento del error en la predicción de la utilidad de las acciones. Como podemos ver, esto no es realmente algo bueno. Por lo tanto, necesitaremos un mecanismo estacionario para valorar la utilidad de las acciones futuras.

Podríamos solucionar esta cuestión creando un modelo adicional para predecir la utilidad de la acción futura, pero este enfoque supondría costes de entrenamiento adicionales para el segundo modelo, y no querríamos eso. Por otro lado, ya estamos entrenando un modelo que ejecuta esta funcionalidad. Solo necesitaremos que, después de cambiar los pesos, el modelo retorne los valores de la función como antes de la actualización. Este contradictorio problema se resolverá copiando el modelo. Simplemente estaremos creando 2 copias del mismo modelo de función de utilidad. Una de ellas se entrenará, mientras que la otra se utilizará para predecir la utilidad de una acción futura.

Sin embargo, una vez que se fija un modelo de la función de utilidad de la acción, pronto se volverá irrelevante en el proceso de aprendizaje, y esto tendrá el potencial de hacer que el aprendizaje continuo resulte ineficaz. Para excluir la influencia de este factor, deberemos actualizar el modelo de predicción de valores durante el entrenamiento. Así, no entrenaremos una segunda copia del modelo en paralelo, sino que simplemente iremos copiando en él los pesos de la copia entrenada del modelo de función de utilidad de la acción a intervalos regulares. De esta forma, entrenando solo un modelo obtendremos de forma bastante actualizada 2 copias del modelo de función de utilidad de la acción y evitaremos la sobreestimación recursiva de los valores predichos. 

Bien, vamos a resumir lo anteriormente dicho:

  1. Para entrenar al agente, utilizaremos una red neuronal.
  2. La red neuronal se entrenará para predecir el valor esperado de la función Q de utilidad de la acción.
  3. Para minimizar la correlación entre los estados vecinos durante el aprendizaje, utilizaremos un búfer de memoria del que recuperaremos los estados de forma aleatoria.
  4. Para predecir el valor futuro de la función Q durante el entrenamiento, se utilizará un segundo modelo de Target Net, que será una copia "congelada" del modelo entrenado.
  5. La actualización de la Target Net se realizará copiando periódicamente las matrices de coeficientes de peso del modelo entrenado.

A continuación, analizaremos la aplicación del enfoque descrito usando MQL5.


3. Aplicación usando MQL5

Implementaremos el aprendizaje Q profundo usando MQL5 en el archivo del asesor experto "Q-learning.mq5". El lector podrá encontrar el código completo del asesor en el archivo adjunto. De momento, nos centraremos únicamente en la implementación del método de aprendizaje Q profundo.

Antes de empezar a aplicarlo, deberemos establecer los datos de origen y el sistema de recompensas. Y si utilizamos los mismos datos de origen que en todos los experimentos anteriores, deberemos reflexionar sobre el sistema de recompensas. El problema de la predicción de fractales del que hablamos antes es bastante artificial. Obviamente, podemos ajustar el modelo para definir tantos fractales como sea posible, pero nuestro principal objetivo es maximizar los beneficios de las transacciones.

En este contexto, es razonable usar el tamaño de la siguiente vela como tamaño de la recompensa. Por supuesto, el signo de la recompensa deberá corresponderse con la transacción realizada. En el modelo simplificado, tenemos 2 operaciones: compra y venta, y también podemos encontrarnos fuera de posición.

No vamos a complicar ahora el modelo definiendo el volumen de posiciones, el incremento de posiciones y los cierres parciales. Consideraremos que el agente puede estar en una posición de lote fijo, o bien que se cierran todas las posiciones y se mantiene fuera del mercado.

Además, a la hora de diseñar una política de recompensas, debemos ser conscientes de que un sistema de recompensas bien diseñado tendrá gran influencia en el resultado del entrenamiento. La práctica del aprendizaje por refuerzo es bastante rica en ejemplos en los que una política de recompensa errónea ha provocado resultados inesperados. Y es que un modelo puede aprender a sacar conclusiones erróneas, o puede obsesionarse con maximizar las recompensas sin lograr el resultado deseado. Por ejemplo, podemos dar a un modelo una recompensa por abrir y cerrar una posición, pero si esta recompensa es mayor que la recompensa por el beneficio total de la transacción, el modelo podría aprender simplemente a abrir y cerrar posiciones. Así, el modelo maximizará las recompensas y nosotros las pérdidas.

Por otro lado, si penalizamos al modelo por abrir y cerrar una posición por analogía a una comisión por operación, el modelo podría aprender simplemente a "quedarse fuera del mercado", sin ingresos, pero sin pérdidas.

Teniendo en cuenta todo lo mencionado, hemos decidido crear un modelo con 3 posibles acciones: Compra, venta, fuera del mercado.

El agente pronosticará la dirección del movimiento esperado en cada nueva vela y elegirá una acción sin tener en cuenta los movimientos anteriores. Es decir, para simplificar el modelo, no suministraremos al agente información sobre si está en posición y en qué dirección. Por lo tanto, el agente no controlará la apertura y el cierre de la posición. Asimismo, no le ofreceremos recompensa alguna por la apertura y el cierre de posiciones.

Para minimizar el tiempo "fuera del mercado", introduciremos una penalización por la ausencia de posición. Obviamente, esta penalización será menor que la de una posición con pérdidas.

Por ello, hemos elaborado la siguiente política de recompensas de los agentes:

  1. Una posición rentable recibirá una recompensa igual al cuerpo de la vela (analizaremos el estado del sistema cada vela y nos encontraremos en una posición desde la apertura de la vela hasta su cierre).
  2. Estar "fuera del mercado" se penalizará con el tamaño del cuerpo de la vela (el tamaño del cuerpo de la vela con signo negativo supondrá la pérdida de beneficios).
  3. Una posición perdedora se penalizará con el doble del tamaño del cuerpo de la vela (pérdidas + beneficios omitidos).

Tras definir el sistema de recompensas, pasaremos directamente a la aplicación del método.

Como hemos mencionado antes, el modelo a construir utilizará 2 redes neuronales. Para ello, crearemos 2 objetos de red neuronal. Entrenaremos StudyNet, mientras que TargetNet se utilizará para predecir los valores futuros de la función Q.

CNet                StudyNet;
CNet                TargetNet;

Para que el método de aprendizaje Q profundo funcione, también necesitaremos nuevas variables externas que definirán los hiperparámetros de la construcción y el entrenamiento del modelo.

  • Batch — tamaño del lote de actualización de los coeficientes de peso;
  • UpdateTarget — número de actualizaciones de las matrices de coeficientes de peso del modelo entrenado antes de copiar al modelo «congelado» que pronosticará los valores futuros de la función Q;
  • Iterations — número total de iteraciones de actualización del modelo de entrenamiento durante el mismo;
  • DiscountFactor — factor de descuento de las recompensas futuras.
input int                  Batch =  100;
input int                  UpdateTarget = 20;
input int                  Iterations = 1000;
input double               DiscountFactor =   0.9;

La creación real del modelo de red neuronal quedará fuera del alcance de este asesor. Para crearla, usaremos la herramienta de los artículos sobre el aprendizaje por transferencia. Este enfoque nos permitirá experimentar con el uso de modelos de diferentes arquitecturas sin tener que modificar el asesor. Por consiguiente, en el método de inicialización del asesor, solo organizaremos la carga del modelo previamente creado.

//---
   float temp1, temp2;
   if(!StudyNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false) ||
      !TargetNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
      return INIT_FAILED;

Tenga en cuenta que como vamos a usar 2 copias del mismo modelo, estaremos cargando ambos modelos desde el mismo archivo.

La libertad de uso de diferentes arquitecturas de modelos implica no solo la capacidad de utilizar diferentes arquitecturas y tamaños de capas ocultas, sino también la posibilidad de ajustar la profundidad de la historia analizada. Y si antes creábamos un modelo en el código del asesor, y la profundidad de la historia se definía según un parámetro externo, ahora podemos determinar la profundidad de la historia analizada según el tamaño de la capa de datos de origen. El asesor, por su parte, lo determinará de forma analítica, basándose en el tamaño de la capa de datos de entrada. Solo el número de neuronas por vela en la historia analizada y el tamaño de la capa de resultados permanecerán inalterados. Estos parámetros estarán estructuralmente relacionados con los indicadores usados y el número de acciones previsibles.

   if(!StudyNet.GetLayerOutput(0, TempData))
      return INIT_FAILED;
   HistoryBars = TempData.Total() / 12;
   StudyNet.getResults(TempData);
   if(TempData.Total() != Actions)
      return INIT_PARAMETERS_INCORRECT;

Es cierto, no hemos discutido previamente el tamaño de la capa de origen en el modelo de aprendizaje Q profundo. Como hemos mencionado antes, la función Q retorna la recompensa esperada en función del estado y la acción realizada, y para determinar la acción más útil, deberemos calcular el valor de la función para todas las acciones posibles en el estado actual. El uso de una red neuronal nos permitirá crear una capa de resultados con un número de neuronas igual al número de todas las acciones posibles. En este caso, cada neurona de la capa de resultados será responsable de pronosticar la utilidad de una acción concreta. Esto nos dará el valor de la utilidad de todas las acciones en una sola pasada de la red neuronal, y todo lo que deberemos hacer es elegir el valor máximo.

En caso contrario, la función de inicialización del asesor no se modificará. Podrá encontrar su código completo en el archivo adjunto.

El proceso de aprendizaje del modelo lo estableceremos en la función Train. Al inicio del cuerpo de esta función, definiremos el tamaño de la sesión de entrenamiento y cargaremos los datos históricos, como hemos hecho antes en el aprendizaje supervisado y no supervisado.

void Train(void)
  {
//---
   MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year -= StudyPeriod;
   if(start_time.year <= 0)
      start_time.year = 1900;
   datetime st_time = StructToTime(start_time);
//---
   int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
      ExpertRemove();
      return;
     }
   if(!ArraySetAsSeries(Rates, true))
     {
      ExpertRemove();
      return;
     }
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();

Debemos decir que al utilizar datos históricos para entrenar el modelo, podemos evitar la creación de un búfer de memoria. Al fin y al cabo, podemos usar todos los datos históricos descargados como un único búfer de memoria. En cambio, en el caso del entrenamiento de modelos en tiempo real, deberemos añadir un búfer de memoria y organizar su proceso de mantenimiento.

A continuación, prepararemos las variables auxiliares:

  • total — tamaño de la muestra de entrenamiento;
  • use_target — bandera para utilizar la Target Net para predecir futuras recompensas.

   int total = bars - (int)HistoryBars - 240;
   bool use_target = false;

El uso de la bandera use_target se debe a la necesidad de desactivar la predicción de futuras recompensas antes de la primera actualización del modelo Target Net. En realidad, se trata de un momento muy sutil. Al fin y al cabo, nuestro modelo se inicializa con coeficientes de peso aleatorios, y esto significa que sus valores predichos también serán completamente aleatorios, por lo que es probable que estén muy lejos de los valores reales. El uso de estos valores aleatorios solo puede distorsionar el proceso de aprendizaje del modelo. El modelo no se aproximará entonces a los verdaderos valores de las recompensas, sino a los valores aleatorios integrados en el propio modelo. Por consiguiente, antes de iterar por primera vez la actualización del modelo Target Net, nos beneficiará más la exclusión de este ruido.

A continuación, organizaremos el sistema de ciclos de entrenamiento del agente. El ciclo exterior contará el número total de iteraciones de la actualización de la matriz de pesos de nuestro agente.

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter += UpdateTarget)
     {
      int i = 0;

En el ciclo anidado, calcularemos el tamaño del paquete de actualización de los coeficientes de peso y el número de actualizaciones antes de que se actualice la Target Net. Aquí debemos destacar que nuestro modelo implementa una actualización de los coeficientes de peso en cada iteración de la pasada inversa. Por ello, usar un paquete de actualización probablemente no parezca del todo correcto, ya que en nuestro modelo siempre será igual a "1". No obstante, para equilibrar el número de estados procesados entre las actualizaciones de Target Net, su periodicidad será igual al producto del tamaño del paquete por el número de actualizaciones entre ellas.

En el cuerpo del ciclo, determinaremos aleatoriamente el estado del sistema para la iteración actual del entrenamiento del modelo. Aquí también limpiaremos los búferes para escribir los 2 estados siguientes. El primer estado se usará para la pasada directa del modelo entrenado, mientras que el segundo se utilizará para los valores pronosticados de la función Q en Target Net.

      for(int batch = 0; batch < Batch * UpdateTarget; batch++)
        {
         i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
         State1.Clear();
         State2.Clear();
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;

A continuación, en un ciclo anidado, llenaremos los búferes preparados con datos históricos. Para evitar operaciones innecesarias, antes de llenar el segundo búfer de estado, comprobaremos la bandera de uso de Target Net. El búfer solo se llenará si es necesario.

         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!State1.Add((float)Rates[bar_t].close - open) || !State1.Add((float)Rates[bar_t].high - open) ||
               !State1.Add((float)Rates[bar_t].low - open) || !State1.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !State1.Add(sTime.hour) || !State1.Add(sTime.day_of_week) || !State1.Add(sTime.mon) ||
               !State1.Add(rsi) || !State1.Add(cci) || !State1.Add(atr) || !State1.Add(macd) || !State1.Add(sign))
               break;
            if(!use_target)
               continue;
            //---
            bar_t --;
            open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            rsi = (float)RSI.Main(bar_t);
            cci = (float)CCI.Main(bar_t);
            atr = (float)ATR.Main(bar_t);
            macd = (float)MACD.Main(bar_t);
            sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!State2.Add((float)Rates[bar_t].close - open) || !State2.Add((float)Rates[bar_t].high - open) ||
               !State2.Add((float)Rates[bar_t].low - open) || !State2.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !State2.Add(sTime.hour) || !State2.Add(sTime.day_of_week) || !State2.Add(sTime.mon) ||
               !State2.Add(rsi) || !State2.Add(cci) || !State2.Add(atr) || !State2.Add(macd) || !State2.Add(sign))
               break;
           }

Una vez que los búferes se hayan llenado satisfactoriamente con los datos históricos, comprobaremos el tamaño de los mismos y realizaremos una pasada directa de ambos modelos. En este caso, no deberemos olvidarnos de comprobar el resultado de la ejecución de las operaciones.

         if(IsStopped())
           {
            ExpertRemove();
            return;
           }
         if(State1.Total() < (int)HistoryBars * 12 ||
            (use_target && State2.Total() < (int)HistoryBars * 12))
            continue;
         if(!StudyNet.feedForward(GetPointer(State1), 12, true))
            return;
         if(use_target)
           {
            if(!TargetNet.feedForward(GetPointer(State2), 12, true))
               return;
            TargetNet.getResults(TempData);
           }

Tras realizar con éxito la pasada directa, obtendremos una recompensa del entorno y prepararemos el búfer de valores objetivo para la pasada inversa según la política de recompensa definida anteriormente.

Aquí hay dos puntos a tener en cuenta. En primer lugar, comprobaremos la bandera de uso de Target Net, y solo añadiremos un valor pronosticado si el resultado es positivo. Si la bandera está en la posición false, los valores pronosticados de la función Q se pondrán a "0".

El segundo punto consiste en el alejamiento respecto a la ecuación de Bellman. Como recordará, la ecuación de Bellman toma el valor máximo de la futura recompensa. De este modo, el modelo se entrenará para maximizar la rentabilidad. Este enfoque, por supuesto, posibilita la obtención de la máxima rentabilidad, pero en el caso del trading, cuando los gráficos de precios están saturados de ruido, esto provocará el aumento de las operaciones, y la presencia de ruido reducirá la calidad de los pronósticos. Podemos comparar esto con el intento de predecir cada nueva vela, lo que potencialmente lleva a la apertura y el cierre de una posición en casi cada nueva vela, en lugar de identificar una tendencia y abrir una posición en su dirección.

Para eliminar la influencia del factor anterior, hemos decidido desviarnos de la ecuación de Bellman y utilizar valores unidireccionales para actualizar el modelo de la función Q. Solo hemos utilizado el máximo para la acción "Fuera del mercado".

         Rewards.Clear();
         double reward = Rates[i - 1 + 240].close - Rates[i - 1 + 240].open;
         if(reward >= 0)
           {
            if(!Rewards.Add((float)(reward + (use_target ? DiscountFactor * TempData.At(0) : 0))) ||
               !Rewards.Add((float)(-2 * (use_target ? reward + DiscountFactor * TempData.At(1) : 0)))
               ||
               !Rewards.Add((float)(-reward + (use_target ? DiscountFactor * TempData.At(TempData.Maximum(0, 3)) : 0))))
               return;
           }
         else
            if(!Rewards.Add((float)(2 * reward + (use_target ? DiscountFactor * TempData.At(0) : 0))) ||
               !Rewards.Add((float)(-reward + (use_target ? DiscountFactor * TempData.At(1) : 0))) ||
               !Rewards.Add((float)(reward + (use_target ? DiscountFactor * TempData.At(TempData.Maximum(0, 3)) : 0))))
               return;

Después de preparar el búfer de recompensa, realizaremos una pasada inversa del modelo entrenado. Una vez más, comprobaremos el resultado de la operación.

         if(!StudyNet.backProp(GetPointer(Rewards)))
            return;
        }

Esto completará el ciclo anidado de la cuenta atrás de las iteraciones de entrenamiento de nuestro agente. Una vez completado, actualizaremos el modelo Target Net. Nuestros modelos no tienen métodos de intercambio de coeficientes de peso. En este caso, tampoco hemos inventado algo nuevo y grandioso. En su lugar, hemos decidido utilizar un mecanismo existente para guardar y cargar el modelo. Al fin y al cabo, estamos obteniendo una copia exacta del modelo, con todo su contenido.

Así que simplemente guardaremos el modelo de entrenamiento en un archivo, y luego cargaremos el modelo guardado desde el archivo en TargetNet. Al mismo tiempo, no deberemos olvidar comprobar el resultado de la ejecución de las operaciones.

      if(!StudyNet.Save(FileName + ".nnw", StudyNet.getRecentAverageError(), 0, 0, Rates[i].time, false))
         return;
      float temp1, temp2;
      if(!TargetNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
         return;
      use_target = true;
      PrintFormat("Iteration %d, loss %.5f", iter, StudyNet.getRecentAverageError());
     }

Una vez que el modelo TargetNet se haya actualizado con éxito, cambiaremos su bandera de uso, mostraremos un mensaje informativo en el diario de registro y pasaremos a la siguiente iteración del ciclo exterior.

Al final del proceso de aprendizaje, eliminaremos los comentarios e iniciaremos el cierre del asesor de entrenamiento del modelo.

   Comment("");
//---
   ExpertRemove();
  }

Podrá encontrar el código completo del asesor en el archivo adjunto.


4. Simulación

Hemos probado el método con EURUSD y en el marco temporal H1 durante los últimos 2 años, por cierto, como en todos los experimentos anteriores. Como parámetros del indicador, hemos utilizado los predeterminados en el asesor.

Asimismo, hemos creado para las pruebas un modelo de convolución con la siguiente arquitectura:

  1. Capa de datos de origen, 240 elementos (20 velas, 12 neuronas para la descripción de una vela).
  2. Capa de convergencia, ventana de datos de origen 24 (2 velas), paso 12 (1 vela), 6 filtros en la salida.
  3. Capa de convergencia, ventana de datos de origen 2, paso 1, 2 filtros.
  4. Capa de convergencia, ventana de datos de origen 3, paso 1, 2 filtros.
  5. Capa de convergencia, ventana de datos de origen 3, paso 1, 2 filtros.
  6. Una capa neuronal completamente conectada de 1 000 elementos.
  7. Una capa neuronal completamente conectada de 1 000 elementos.
  8. Una capa completamente conectada de 3 elementos (capa de resultados de las 3 acciones).

Las capas 2 a 7 fueron activadas por el sigmoide. Para la capa de resultados, se utilizó la tangente hiperbólica como función de activación.

En el siguiente gráfico, podemos ver la dinámica del error durante el entrenamiento del modelo. Como verá en el gráfico, el error en la predicción de la recompensa esperada tiende a bajar bastante rápido durante la curva de aprendizaje. Y después de 500 iteraciones, se acerca a "0". El proceso de entrenamiento del modelo de 1 000 iteraciones ha finalizado con un error de 0,00105.

Gráfico de pruebas del modelo DQN


Conclusión

En este artículo, hemos continuado nuestra introducción a los métodos de aprendizaje por refuerzo. Así, hemos analizado el método de aprendizaje Q profundo introducido por el equipo de DeepMind en 2013. Con la aparición de este método, podemos decir que se inició una nueva fase en el desarrollo de los algoritmos de aprendizaje por refuerzo. El uso de este método ha demostrado que resulta posible entrenar modelos para construir estrategias. El uso de un único modelo permite entrenar este para resolver diferentes problemas sin realizar cambios estructurales en su arquitectura o en sus hiperparámetros. Estos fueron los primeros experimentos en los que un algoritmo entrenado superó a un experto.

Hemos analizado la aplicación del método usando MQL5, y los resultados obtenidos al probar el modelo demuestran que puede utilizarse para construir modelos comerciales que funcionan.


Enlaces

  1. Playing Atari with Deep Reinforcement Learning
  2. Redes neuronales: así de sencillo (Parte 25): Practicando el Transfer Learning
  3. Redes neuronales: así de sencillo (Parte 26): Aprendizaje por refuerzo

Programas usados en el artículo.

# Nombre Tipo Descripción
1 Q-learning.mq5 Asesor Asesor para el entrenamiento de modelos 
2 NeuroNet.mqh Biblioteca de clases Biblioteca para organizar modelos de redes neuronales
3 NeuroNet.cl Biblioteca
Biblioteca de código OpenCL para organizar modelos de redes neuronales


Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/11369

Archivos adjuntos |
MQL5.zip (66.7 KB)
Aprendiendo a diseñar un sistema de trading con VIDYA Aprendiendo a diseñar un sistema de trading con VIDYA
Bienvenidos a un nuevo artículo de la serie dedicada a la creación de sistemas comerciales basados en indicadores técnicos populares. En este artículo hablaremos sobre el indicador VIDYA (Variable Index Dynamic Average) y crearemos un sistema comercial basado en sus lecturas.
DoEasy. Elementos de control (Parte 16): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, modo de expansión de encabezados para ajustarse al tamaño del contenedor DoEasy. Elementos de control (Parte 16): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, modo de expansión de encabezados para ajustarse al tamaño del contenedor
En este artículo, proseguiremos con el desarrollo del control TabControl, e implementaremos la disposición de los encabezados de las pestañas en los cuatro lados del control para todos los modos de establecimiento de tamaño del encabezado: "Normal", "Fixed" y "Fill To Right".
Redes neuronales: así de sencillo (Parte 28): Algoritmo de gradiente de políticas Redes neuronales: así de sencillo (Parte 28): Algoritmo de gradiente de políticas
Continuamos analizando los métodos de aprendizaje por refuerzo. En el artículo anterior, nos familiarizamos con el método de aprendizaje Q profundo, en el que entrenamos un modelo para predecir la próxima recompensa dependiendo de la acción realizada en una situación particular. Luego realizamos una acción según nuestra política y la recompensa esperada, pero no siempre es posible aproximar la función Q, o su aproximación no ofrece el resultado deseado. En estos casos, los métodos de aproximación no se utilizan para funciones de utilidad, sino para una política (estrategia) de acciones directa. Precisamente a tales métodos pertenece el gradiente de políticas o policy gradient.
Redes neuronales: así de sencillo (Parte 26): Aprendizaje por refuerzo Redes neuronales: así de sencillo (Parte 26): Aprendizaje por refuerzo
Continuamos estudiando los métodos de aprendizaje automático. En este artículo, iniciaremos otro gran tema llamado «Aprendizaje por refuerzo». Este enfoque permite a los modelos establecer ciertas estrategias para resolver las tareas. Esperamos que esta propiedad del aprendizaje por refuerzo abra nuevos horizontes para la construcción de estrategias comerciales.