English Русский 中文 Deutsch 日本語 Português
preview
Redes neuronales: así de sencillo (Parte 71): Previsión de estados futuros basada en objetivos (GCPC)

Redes neuronales: así de sencillo (Parte 71): Previsión de estados futuros basada en objetivos (GCPC)

MetaTrader 5Sistemas comerciales | 4 julio 2024, 08:27
202 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

El aprendizaje simulado (Behavior Cloning — BC) dirigido por objetivos es un enfoque prometedor para diversas tareas de aprendizaje por refuerzo offline. En lugar de evaluar el valor del estado y la acción, el BC entrena directamente la política de comportamiento del Agente construyendo dependencias entre el objetivo, el estado del entorno analizado y la acción del Agente. Esto se consigue usando métodos de aprendizaje supervisados sobre trayectorias offline preconstruidas. El conocido método del Decision Transformer y sus algoritmos derivados han demostrado la eficacia del modelado de secuencias para el aprendizaje por refuerzo offline.

Antes, al utilizar los algoritmos mencionados, experimentamos con distintas variantes de fijación de objetivos para inducir las acciones del Agente que necesitamos. El aprendizaje de una trayectoria ya recorrida por parte del modelo siempre ha permanecido ajeno a nuestra atención. Y aquí cabe preguntarse por la utilidad de los estudios de trayectoria en general. Los autores del artículo «Goal-Conditioned Predictive Coding for Offline Reinforcement Learning» se plantearon una pregunta similar. En su artículo exploran varias cuestiones clave:

  1. ¿Resultan útiles las trayectorias offline para el modelado de secuencias o solo aportan más datos para el aprendizaje supervisado de políticas?

  2. ¿Cuáles serían los objetivos de aprendizaje más eficaces de la representación de trayectorias para apoyar el entrenamiento de la política? ¿Deben entrenarse los modelos secuenciales para codificar la experiencia de la historia, la dinámica futura, o ambas?

  3. Como el propio modelo de secuencia puede utilizarse tanto para el entrenamiento de representación de trayectorias como para el entrenamiento de políticas, ¿deberemos tener los mismos objetivos de aprendizaje o no?

El artículo presenta los resultados de varios experimentos en 3 entornos artificiales, que permiten a los autores extraer las siguientes conclusiones:

  • El modelado de secuencias, si se diseña correctamente, puede ayudar eficazmente a la toma de decisiones cuando la representación de la trayectoria resultante se utiliza como entrada para el entrenamiento de políticas;

  • existe un desajuste entre el objetivo de aprendizaje óptimo de la representación de la trayectoria y el objetivo de aprendizaje de la política.

Estas observaciones llevan a los autores del artículo a desarrollar un marco de dos etapas que comprime la información de la trayectoria en representaciones compactas comprimidas usando un modelado de secuencias previo al entrenamiento. A continuación, la representación comprimida se utilizará para entrenar la política de comportamiento del Agente usando un sencillo modelo basado en un perceptrón multicapa (MLP). El método Goal-Conditioned Predictive Coding (GCPC) propuesto es el más eficaz para aprender a representar trayectorias. Ofrece un rendimiento competitivo en todas las tareas de prueba realizadas por los autores. Destaca por su eficacia para resolver problemas con horizontes de planificación largos. El sólido rendimiento empírico del GCPC se obtiene gracias a la representación latente de los estados recorridos y predichos. De esta forma, la previsión del estado se basa en objetivos, lo cual proporciona una orientación crucial para la toma de decisiones.

1. El algoritmo Goal-Conditioned Predictive Coding

Los autores del método GCPC utilizan en su trabajo el modelado de secuencias para el aprendizaje por refuerzo offline. Para resolver el problema del aprendizaje de refuerzo offline, se usa el aprendizaje simulado condicional, filtrado o ponderado. Se presupone que disponemos de un conjunto de datos de entrenamiento previamente recopilados, pero es posible que no se conozcan las políticas utilizadas para recopilar los datos. Los datos de entrenamiento contienen un conjunto de trayectorias, y cada trayectoria se representa como un conjunto de estados y acciones (St, At). La trayectoria puede contener opcionalmente la recompensa Rt obtenida en el paso temporal t.

Como las trayectorias se recogen utilizando políticas desconocidas, es posible que no resulten óptimas o no tengan suficiente experiencia. Ya hemos comentado que el uso adecuado de trayectorias offline que contienen datos subóptimos puede conducir a políticas de comportamiento más eficaces. Puesto que las trayectorias subóptimas pueden contener subtrayectorias que demuestren "habilidades" útiles que pueden combinarse para resolver las tareas planteadas.

Los autores del método creen que la política de comportamiento del Agente debe ser capaz de tomar cualquier forma de información del estado o la trayectoria como entrada y predecir la siguiente acción:

  • cuando solo se utilizan el estado actual observado St y el objetivo G, la política del Agente ignora la historia de observación; 
  • cuando la política del Agente es un modelo de secuencia, puede utilizar toda la trayectoria observada para predecir la siguiente acción At.

Normalmente se usa una función objetivo de máxima verosimilitud para optimizar la política de comportamiento del Agente.

La modelización de secuencias puede utilizarse para tomar decisiones desde dos perspectivas: el aprendizaje de representaciones de trayectorias y el aprendizaje de políticas de comportamiento. La primera dirección busca obtener representaciones útiles partiendo de trayectorias de entrada brutas en forma de representación latente comprimida o de pesos de una red preentrenada. La segunda dirección trata de transformar la observación y el propósito en una acción óptima para la tarea.

El entrenamiento de la función de trayectoria y de la función de política puede realizarse utilizando modelos de Transformer. Los autores del método GCPC sugieren que resulta útil para la función de trayectoria comprimir los datos originales en una representación compacta utilizando técnicas de modelado de secuencias. También resulta cómodo separar el aprendizaje de la representación de la trayectoria y el aprendizaje de la política. La separación no solo ofrece flexibilidad en la elección de los objetivos del aprendizaje de representación, sino que también nos permite estudiar el impacto del modelado de secuencias en el aprendizaje de representación de trayectorias y el aprendizaje de políticas de forma independiente. Por ello, el GCPC utiliza una estructura de dos etapas con TrajNet (modelo de trayectoria) y PolicyNet (modelo de política). La formación de TrajNet usa técnicas de aprendizaje no supervisado para el modelado de secuencias, como el autocodificador enmascarado o la predicción del siguiente token. PolicyNet pretende obtener políticas eficaces utilizando la función objetivo de aprendizaje supervisado a partir de las trayectorias offline recopiladas.

En la primera etapa del entrenamiento de la representación de la trayectoria, se utiliza la autocodificación enmascarada. TrajNet recibe la trayectoria y, si es necesario, el objetivo G, y se entrena para reconstruir τ partiendo de una forma enmascarada de la misma trayectoria. Opcionalmente, TrajNet también genera una representación comprimida de la trayectoria B, que puede ser usada por PolicyNet para el posterior entrenamiento de políticas. En su trabajo, los autores del método GCPC proponen suministrar una representación enmascarada de la trayectoria recorrida a la entrada del modelo de autocodificador. En este caso, la salida del decodificador buscará una representación no enmascarada de la trayectoria recorrida y los estados posteriores.

En el segundo paso, TrajNet se aplica a la trayectoria observada desenmascarada para obtener una representación comprimida de la trayectoria B. PolicyNet predice entonces la acción A dada la trayectoria recorrida (o estado actual del entorno), el objetivo G y una representación comprimida de la trayectoria B.

El marco propuesto ofrece una representación unificada para comparar diferentes diseños de implementación del aprendizaje de representaciones y el aprendizaje de políticas. Muchos métodos existentes pueden considerarse casos especiales del marco propuesto. Por ejemplo, para la implementación de DT, la función de representación de la trayectoria se establece como la función de mapeo de identidad de la trayectoria de entrada, mientras que la política se entrena para generar acciones de forma autorregresiva.

A continuación le presentamos el método de autor de la visualización.

2. Implementación con MQL5

Tras considerar los aspectos teóricos del método Goal-Conditioned Predictive Coding, vamos a pasar a su implementación usando MQL5. Y aquí lo primero que debemos señalar es el diferente número de modelos utilizados en las distintas fases de entrenamiento y explotación de los modelos.

2.1 Arquitectura del modelo

Como primer paso, los autores del método proponen entrenar un modelo de representaciones de trayectorias. La arquitectura de este modelo usa el Transformer. Y para entrenarlo, necesitaremos construir un Autocodificador. En este segundo paso, solo usaremos el codificador entrenado. Por lo tanto, para no "arrastrar" un Decodificador innecesario a la segunda etapa de entrenamiento vamos a dividir Autocodificador en 2 modelos: Codificador y Decodificador. Representaremos la arquitectura de los modelos en el método CreateTrajNetDescriptions. En los parámetros, el método obtiene los punteros a 2 arrays dinámicos para especificar la arquitectura de los modelos indicados.

En el cuerpo del método comprobaremos los punteros obtenidos y, si es necesario, crearemos nuevos objetos de array dinámicos.

bool CreateTrajNetDescriptions(CArrayObj *encoder, CArrayObj *decoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!decoder)
     {
      decoder = new CArrayObj();
      if(!decoder)
         return false;
     }

En primer lugar, describiremos la arquitectura del Codificador. Solo introduciremos en el modelo los datos históricos sobre los movimientos de precio y los indicadores analizados.  

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Nótese que, a diferencia de los modelos comentados anteriormente, en esta fase no utilizaremos ni los datos sobre el estado de la cuenta ni la información sobre las acciones anteriores del Agente. Existe la percepción de que, en algunos casos, la información sobre las acciones pasadas puede tener un impacto negativo. Así que los autores del método GCPC la excluyeron de los datos brutos. En cambio, la información de la cuenta no influye en las condiciones del entorno. En consecuencia, resulta irrelevante para predecir estados del entorno posteriores.

Como siempre, introduciremos los datos de entrada en el modelo sin procesar. Por lo tanto, la siguiente capa que usaremos es la normalización por lotes para transformar los datos originales en una forma comparable.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Tras el preprocesamiento de los datos, deberemos aplicar el enmascaramiento aleatorio de datos que proporciona el algoritmo GCPC. Para implementar esta funcionalidad, utilizaremos una capa DropOut.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDropoutOCL;
   descr.count = prev_count;
   descr.probability = 0.8f;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Cabe señalar aquí que, en la práctica general, no se recomienda utilizar las capa de normalización por lotes y DropOut juntas en el mismo modelo. Esto se debe a que excluir parte de la información y sustituirla por valores cero distorsiona la distribución original de los datos y repercute negativamente en el rendimiento de la capa de normalización por lotes. Por este motivo, primero normalizaremos los datos y solo después enmascararemos. De este modo, la capa de normalización por lotes trabajará con el conjunto de datos completo y minimizará el impacto de la capa DropOut en su funcionamiento. Al mismo tiempo, implementaremos la funcionalidad de enmascaramiento para enseñar a nuestro modelo a recuperar los datos que faltan e ignorar los "valores atípicos" inherentes a un entorno estocástico.

Lo siguiente en nuestro modelo de codificador será un bloque de convolución para reducir la dimensionalidad de los datos y extraer patrones estables.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   int prev_wout = descr.window_out = BarDescr / 2;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = 8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

El resultado del procesamiento de los datos brutos descrito anteriormente se introducirá en el bloque de la capa completamente conectada, que nos permitirá obtener la incorporación del estado inicial.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Además de los datos históricos, los autores del método GCPC proponen suministrar a la entrada del Codificador la incorporación del objetivo y Slot tokens (resultados de las anteriores pasadas del Codificador). Nuestro objetivo global de generar los máximos ingresos posibles no tiene impacto en el entorno, así que lo omitiremos. Y aquí añadiremos los resultados de la última pasada de nuestro Codificador al modelo usando la capa de concatenación.

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = 2 * EmbeddingSize;
   descr.window = prev_count;
   descr.step = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

El procesamiento posterior de los datos se realizará utilizando el modelo GPT. Para implementarlo, primero crearemos una pila de datos utilizando la capa de incorporación.

//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronEmbeddingOCL;
   prev_count = descr.count = GPTBars;
     {
      int temp[] = {EmbeddingSize, EmbeddingSize};
      ArrayCopy(descr.windows, temp);
     }
   prev_wout = descr.window_out = EmbeddingSize;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Después vendrá un bloque de atención. Antes ya hemos creado la dispersión de datos con la capa DropOut, así que no utilizaremos la capa de atención dispersa en este modelo. 

//--- layer 11
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMLMHAttentionOCL;
   descr.count = prev_count * 2;
   descr.window = prev_wout;
   descr.step = 4;
   descr.window_out = 16;
   descr.layers = 4;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

A la salida del codificador, redimensionaremos los datos con una capa de completamente conectada y los normalizaremos con la función SoftMax.

//--- layer 12
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 13
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = 1;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Luego suministraremos la representación comprimida de la trayectoria recorrida a la entrada del Decodificador.

//--- Decoder
   decoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Los datos de entrada del descodificador se obtendrán del modelo anterior y ya tienen una forma comparable, lo cual significa que la capa de normalización de lotes será innecesaria en este caso. A continuación, ampliaremos los datos resultantes con una capa totalmente conectada.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = (HistoryBars + PrecoderBars) * EmbeddingSize;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Después de lo cual, realizaremos el procesamiento en la capa de atención.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMLMHAttentionOCL;
   prev_count = descr.count = prev_count / EmbeddingSize;
   prev_wout = descr.window = EmbeddingSize;
   descr.step = 4;
   descr.window_out = 16;
   descr.layers = 2;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Hemos diseñado nuestra arquitectura del Decodificador de modo que tengamos una incorporación para cada vela de los estados de los entornos analizados y predichos a la salida de la unidad de atención. Y aquí es donde tendremos que "separar el grano de la paja". Le sugiero que especulemos un poco.

¿Por qué analizamos los indicadores? Los indicadores de tendencia nos muestran la dirección de la misma. Los osciladores están diseñados para indicar zonas de sobrecompra y sobreventa, mostrando así posibles puntos de inversión del mercado. Todo esto es valioso en el momento actual. Pero ¿cómo de valioso es para nosotros prever esos datos con cierta profundidad? Mi opinión personal es que, dado el error de predicción de los datos, el valor de la predicción de las lecturas de los indicadores se acerca a "0". Al final, obtendremos beneficios y pérdidas dependiendo de los cambios en el precio del instrumento, no de los valores de los indicadores. Por lo tanto, pronosticaremos los datos del movimiento de precios en la salida del descodificador.

Y enseguida tendremos que recordar qué información sobre el movimiento de precios almacenaremos en el búfer de reproducción de experiencias. Son 3 desviaciones:

  • el cuerpo de la vela (close - open)
  • desde el precio de apertura hasta el máximo (high - open)
  • desde el precio de apertura hasta el mínimo (low - open).

Estos son los que pronosticaremos. Para recuperar de forma independiente los valores de incorporación de las velas, utilizaremos la capa de conjunto de modelos.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 3;
   descr.window = prev_wout;
   descr.step = prev_count;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

Con esto concluimos la descripción de la arquitectura del autocodificador para la primera etapa de entrenamiento de la representación de trayectorias de TrajNet. Pero antes de pasar a trabajar sobre los asesores de entrenamiento de modelos, le propongo completar el trabajo sobre la descripción de la arquitectura de los modelos. Echemos ahora un vistazo a la arquitectura de la segunda etapa de los modelos de entrenamiento de la política PolicyNet, que se representa en el método CreateDescriptions.

Debemos decir de entrada que, contrariamente a lo esperado, en la segunda etapa no entrenaremos 1 modelo de política de comportamiento del Actor, sino 3 modelos a la vez.

Un primer modelo pequeño del Codificador de estado actual. No lo confunda con el Codificador del Autocodificador entrenado en el primer paso. Este modelo combinará la representación comprimida de la trayectoria del Codificador del Autocodificador con la información del estado de la cuenta en una única representación.

Un segundo modelo de política del Actor, del que ya hemos hablado mucho.

Y un tercer modelo de fijación de objetivos basado en el análisis de una representación comprimida de la trayectoria.

Como es habitual, en los parámetros del método pasaremos los punteros a los arrays dinámicos de descripción de la arquitectura del modelo. Y en el cuerpo del método comprobaremos la relevancia de los punteros obtenidos y, si es necesario, crearemos nuevos ejemplares de objetos de array dinámicos.

bool CreateDescriptions(CArrayObj *actor, CArrayObj *goal, CArrayObj *encoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!goal)
     {
      goal = new CArrayObj();
      if(!goal)
         return false;
     }
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }

Como ya hemos mencionado, introduciremos una representación comprimida de la trayectoria en la entrada del codificador.

//--- State Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Los datos resultantes se combinarán con la información sobre el estado de la cuenta en la capa de concatenación.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = AccountDescr;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

En este punto, las tareas establecidas para el Codificador se considerarán completadas y podemos pasar a la arquitectura del Actor, que recibe los resultados del modelo anterior como entrada.

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Combinaremos los datos obtenidos con el objetivo establecido.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = NRewards;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Y realizaremos el procesamiento con capas totalmente conectadas.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 2 * NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

En la salida del Actor, añadiremos estocasticidad a su política de comportamiento.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Por último, pero no por ello menos importante, está el modelo de generación de objetivos. Creo que no es ningún secreto que las oportunidades de generar beneficios dependen en gran medida de diversos aspectos del entorno. Por ello, basándonos en la experiencia anterior, hemos decidido añadir un modelo independiente para generar objetivos basados en las condiciones del entorno.

Suministraremos a la entrada del modelo una representación comprimida de la trayectoria recorrida. Nos referimos a las trayectorias sin considerar el estado de la cuenta. Nuestra función de recompensa está diseñada para trabajar con valores relativos sin vinculación a un tamaño de depósito específico. Por lo tanto, también para fijar objetivos, partiremos únicamente del análisis del entorno sin considerar el estado de la cuenta.

//--- Goal
   goal.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!goal.Add(descr))
     {
      delete descr;
      return false;
     }

Los datos resultantes se analizarán en 2 capas totalmente conectadas.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!goal.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!goal.Add(descr))
     {

      delete descr;
      return false;
     }

Y en la salida del modelo, utilizaremos una función cuantil totalmente parametrizada. La ventaja de esta solución es que retorna el resultado más probable en lugar del valor medio inherente a una capa totalmente conectada. Las diferencias en los resultados resultan más notables en las distribuciones con 2 o más picos.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronFQF;
   descr.count = NRewards;
   descr.window_out = 32;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!goal.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

2.2 Modelo de interacción con el entorno

Seguimos trabajando en la aplicación del método Goal-Conditioned Predictive Coding. Y tras describir la arquitectura de los modelos, pasaremos a la aplicación directa de los algoritmos. Primero implementaremos un asesor que se encargará de interactuar con el entorno y recoger datos para la muestra de entrenamiento. Los autores del método no hicieron hincapié en el método de recogida de datos de entrenamiento. De hecho, la muestra de entrenamiento puede recogerse de cualquier forma disponible, incluidos los algoritmos ExORL y Real-ORL que hemos comentado antes. Solo será necesario que coincidan los formatos de registro y de representación de datos. Pero para optimizar los modelos preentrenados, necesitaremos un asesor que utilice nuestra política de comportamiento aprendida durante la interacción con el entorno y almacene los resultados de la interacción en una trayectoria. Implementaremos dicha funcionalidad en el asesor "...\Experts\GCPC\Research.mq5". Los principios básicos del algoritmo del asesor proceden de trabajos anteriores. Sin embargo, el número de modelos en uso imprimirá su propia huella. Vamos a destacar algunos de los métodos del asesor.

En este asesor utilizaremos 4 modelos.

CNet                 Encoder;
CNet                 StateEncoder;
CNet                 Actor;
CNet                 Goal;

Los modelos preentrenados se cargarán en el método de inicialización del asesor OnInit. No vamos a repetir al completo el código del método: podrá leerlo en el archivo adjunto. Nos centraremos en los cambios.

Primero cargaremos el modelo del Codificador Autocodificador. Cuando se produce un error de carga, inicializaremos un nuevo modelo con parámetros aleatorios.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
........
........
//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *encoder = new CArrayObj();
      CArrayObj *decoder = new CArrayObj();
      if(!CreateTrajNetDescriptions(encoder, decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      if(!Encoder.Create(encoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      delete encoder;
      delete decoder;
      //---
     }

A continuación, cargaremos los 3 modelos restantes, que, de ser necesario, también inicializaremos con parámetros aleatorios.

   if(!StateEncoder.Load(FileName + "StEnc.nnw", temp, temp, temp, dtStudied, true) ||
      !Goal.Load(FileName + "Goal.nnw", temp, temp, temp, dtStudied, true) ||
      !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *goal = new CArrayObj();
      CArrayObj *encoder = new CArrayObj();
      if(!CreateDescriptions(actor, goal, encoder))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !StateEncoder.Create(encoder) || !Goal.Create(goal))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      delete actor;
      delete goal;
      delete encoder;
      //---
     }

Luego trasladaremos todos los modelos a un único contexto OpenCL.

   StateEncoder.SetOpenCL(Actor.GetOpenCL());
   Encoder.SetOpenCL(Actor.GetOpenCL());
   Goal.SetOpenCL(Actor.GetOpenCL());

Deberemos desactivar obligatoriamente el modo de entrenamiento del modelo de Codificador.

   Encoder.TrainMode(false);

Tenga en cuenta que, aunque no tenemos previsto utilizar técnicas de pasada inversa en este asesor, el uso de una capa DropOut en el Codificador nos obligará a cambiar el modo de entrenamiento para desactivar el enmascaramiento en condiciones de explotación del modelo.

Y comprobar la coherencia de las arquitecturas de los modelos cargados.

   Actor.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }
   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("The scope of the Encoder does not match the embedding size (%d <> %d)", EmbeddingSize, 
                                                                                                  Result.Total());
      return INIT_FAILED;
     }
//---
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(),
                                                                                        (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   PrevBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   PrevEquity = AccountInfoDouble(ACCOUNT_EQUITY);
//---
   return(INIT_SUCCEEDED);
  }

La interacción con el entorno se realizará en el método OnTick. Al principio del método comprobaremos la aparición de un nuevo evento de apertura de barra y, si es necesario, cargaremos los datos históricos del terminal. Después transferiremos la información obtenida a los búferes de datos. Las operaciones anteriores se han mantenido sin cambios y no nos detendremos en ellas. Consideraremos solo la secuencia de llamada a los métodos de pasada directa del modelo. Como ya prevé el algoritmo GCPC, primero llamaremos al método de pasada directa del codificador.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
........
........
//---
   if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(Encoder)) ||

Obsérvese que el modelo se usa a sí mismo de forma recurrente como fuente de datos para el segundo flujo de información.

A continuación, llamaremos el método de pasada directa del Codificador de estados y el modelo de fijación de objetivos. Ambos modelos usan como datos de entrada una representación comprimida de la trayectoria recorrida.

      !StateEncoder.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat *)GetPointer(bAccount)) ||
      !Goal.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat *)NULL) ||

Los resultados de estos modelos se introducirán en el modelo de política del Actor para generar la acción subsiguiente.

      !Actor.feedForward((CNet *)GetPointer(StateEncoder), -1, (CNet *)GetPointer(Goal)))
      return;

No olvide comprobar los resultados de las operaciones.

A continuación se descodificarán los resultados del modelo del Actor y se realizarán las acciones en el entorno para después guardar la experiencia en una trayectoria. El algoritmo de dichas operaciones se mantendrá sin cambios, así que no nos detendremos ahora en su análisis. Encontrará el código completo del asesor de interacción con el entorno en el archivo adjunto.

2.3 Entrenamiento de la función de trayectoria

Tras recoger los datos de la muestra de entrenamiento, pasaremos a construir los asesores de entrenamiento del modelo. Y según el algoritmo GCPC, el primer paso consistirá en entrenar el modelo de función de trayectoria TrajNet. Implementaremos esta funcionalidad en el asesor "...\Experts\GCPC\StudyEncoder.mq5".

Como ya mencionamos en la parte teórica del presente artículo, en el primer paso entrenaremos un modelo de autocodificador enmascarado, que en nuestro caso constará de 2 modelos: Un Codificador y un Decodificador.

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input int                  Iterations     = 1e4;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
STrajectory          Buffer[];
CNet                 Encoder;
CNet                 Decoder;

Y aquí debemos señalar un punto importante. El codificador usa recursivamente sus propios resultados de la pasada anterior como datos de entrada para el segundo flujo de información. Y mientras que para las pasadas directas podemos utilizar simplemente un puntero al propio modelo, este enfoque no será aceptable para las pasadas inversas. Al fin y al cabo, el búfer de resultados del modelo contendrá los datos de la última pasada, no de la anterior, lo cual no es aceptable para nuestro proceso de entrenamiento de modelos. En consecuencia, necesitaremos un búfer de datos adicional para guardar los resultados de la pasada anterior.

CBufferFloat         LastEncoder;

En el método de inicialización del asesor, primero cargaremos la muestra de entrenamiento y comprobaremos el resultado de las operaciones. Al fin y al cabo, si no hay datos para entrenar los modelos, todas las operaciones posteriores carecerán de sentido.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

Tras cargar correctamente la muestra de entrenamiento, intentaremos abrir los modelos preentrenados. Y si se produce un error, inicializaremos nuevos modelos con parámetros aleatorios.

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) ||
      !Decoder.Load(FileName + "Dec.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Init new models");
      CArrayObj *encoder = new CArrayObj();
      CArrayObj *decoder = new CArrayObj();
      if(!CreateTrajNetDescriptions(encoder, decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      if(!Encoder.Create(encoder) || !Decoder.Create(decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      delete encoder;
      delete decoder;
      //---
     }

 Luego colocaremos ambos modelos en un único contexto OpenCL.

   OpenCL = Encoder.GetOpenCL();
   Decoder.SetOpenCL(OpenCL);

Y comprobaremos que las arquitecturas de los modelos coinciden.

   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("The scope of the Encoder does not match the embedding size count (%d <> %d)", EmbeddingSize,
                                                                                                 Result.Total());
      return INIT_FAILED;
     }
//---
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(),
                                                                                       (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   Decoder.GetLayerOutput(0, Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("Input size of Decoder doesn't match Encoder output (%d <> %d)", Result.Total(), EmbeddingSize);
      return INIT_FAILED;
     }

Tras pasar con éxito el bloque de control, inicializaremos los búferes auxiliares en el mismo contexto OpenCL.

   if(!LastEncoder.BufferInit(EmbeddingSize,0) ||
      !Gradient.BufferInit(EmbeddingSize,0) ||
      !LastEncoder.BufferCreate(OpenCL) ||
      !Gradient.BufferCreate(OpenCL))
     {
      PrintFormat("Error of create buffers: %d", GetLastError());
      return INIT_FAILED;
     }

Al final del método de inicialización del asesor, generaremos un evento personalizado para iniciar el proceso de entrenamiento.

   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

El propio proceso de entrenamiento se llevará a cabo directamente en el método Train. En él, combinaremos tradicionalmente el algoritmo Goal-Conditioned Predictive Coding con nuestros desarrollos de artículos anteriores. Al principio del método, crearemos un vector con las probabilidades de utilizar trayectorias para entrenar los modelos.

//+------------------------------------------------------------------+
//| Train function                                                   |
//+------------------------------------------------------------------+
void Train(void)
  {
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);

Sin embargo, cabe señalar que la ponderación de la trayectoria no tendrá ningún efecto práctico en este caso. Al fin y al cabo, durante el entrenamiento del autocodificador solo utilizaremos los datos históricos de los movimientos de precio y los indicadores analizados. Todas las trayectorias se recogerán en un intervalo histórico del instrumento. Por lo tanto, en la comprensión de nuestro autocodificador, todas las trayectorias contendrán datos idénticos. No obstante, la funcionalidad se ha dejado "con vistas al futuro" para permitir el entrenamiento de modelos con trayectorias de diferentes intervalos de tiempo e instrumentos.

A continuación, inicializaremos las variables locales y los vectores. Aquí llama la atención el vector de desviaciones típicas. Su tamaño es igual al vector de resultados del Decodificador. Sus principios de uso se explicarán un poco más adelante.

   vector<float> result, target;
   matrix<float> targets;
   STD = vector<float>::Zeros((HistoryBars + PrecoderBars) * 3);
   int std_count = 0;
   uint ticks = GetTickCount();

Tras los trabajos preparatorios, organizaremos un sistema de ciclos de entrenamiento de modelos. Recordemos que el codificador utilizará un bloque GPT con una pila de estado latente que es sensible a la secuencia de los datos de origen. Por consiguiente, utilizaremos paquetes enteros de estados consecutivos de cada trayectoria muestreada al entrenar los modelos.

En el cuerpo del ciclo externo, muestrearemos una trayectoria dadas las probabilidades generadas anteriormente y seleccionaremos aleatoriamente un estado inicial en ella.

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int batch = GPTBars + 50;
      int state = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 3 - PrecoderBars - batch));
      if(state <= 0)
        {
         iter--;
         continue;
        }

A continuación, borraremos las pilas de modelos y el búfer de resultados anteriores del codificador.

      Encoder.Clear();
      Decoder.Clear();
      LastEncoder.BufferInit(EmbeddingSize,0);

Ahora todo está listo para iniciar un ciclo de entrenamiento anidado con la trayectoria seleccionada.

      int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
      for(int i = state; i < end; i++)
        {
         State.AssignArray(Buffer[tr].States[i].state);

En el cuerpo del ciclo, llenaremos el búfer de datos sin procesar de la muestra de entrenamiento y llamaremos secuencialmente a los métodos de pasada directa de nuestros modelos. Primero, el codificador.

         if(!LastEncoder.BufferWrite() || !Encoder.feedForward((CBufferFloat*)GetPointer(State), 1, false, 
                                                               (CBufferFloat*)GetPointer(LastEncoder)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

Y luego el Decodificador.

         if(!Decoder.feedForward(GetPointer(Encoder), -1, (CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

Una vez que hemos realizado con éxito la pasada directa de nuestros modelos, tendremos que realizar una pasada inversa con los ajustes de los parámetros de los modelos. Pero primero deberemos preparar los valores objetivo de los resultados del Descodificador. Como recordará, a la salida del decodificador pretendemos obtener los valores reconstruidos y los resultados de las previsiones de las fluctuaciones de precio de varias velas que se especifican en los tres primeros elementos del array de descripción del estado de cada vela. Para obtener estos datos, crearemos una matriz en cada fila de la cual almacenaremos las descripciones de las condiciones del entorno en el intervalo temporal que nos interesa. Y luego tomaremos solo las 3 primeras columnas de la matriz resultante. Estos serán nuestros valores objetivo.

         target.Assign(Buffer[tr].States[i].state);
         ulong size = target.Size();
         targets = matrix<float>::Zeros(1, size);
         targets.Row(target, 0);
         if(size > BarDescr)
            targets.Reshape(size / BarDescr, BarDescr);
         ulong shift = targets.Rows();
         targets.Resize(shift + PrecoderBars, 3);
         for(int t = 0; t < PrecoderBars; t++)
           {
            target.Assign(Buffer[tr].States[i + t].state);
            if(size > BarDescr)
              {
               matrix<float> temp(1, size);
               temp.Row(target, 0);
               temp.Reshape(size / BarDescr, BarDescr);
               temp.Resize(size / BarDescr, 3);
               target = temp.Row(temp.Rows() - 1);
              }
            targets.Row(target, shift + t);
           }
         targets.Reshape(1, targets.Rows()*targets.Cols());
         target = targets.Row(0);

Inspirado por los resultados del artículo anterior sobre el uso de operadores de forma cerrada, he decidido cambiar ligeramente el proceso de entrenamiento y hacer más hincapié en las desviaciones significativas. Dicho esto, simplemente ignoraremos las desviaciones menores como un error de previsión. Por lo tanto, en esta fase calculo la desviación típica media móvil de los resultados del modelo con respecto a los valores objetivo.

         Decoder.getResults(result);
         vector<float> error = target - result;
         std_count = MathMin(std_count, 999);
         STD = MathSqrt((MathPow(STD, 2) * std_count + MathPow(error, 2)) / (std_count + 1));
         std_count++;

Cabe señalar aquí que controlaremos la desviación de cada parámetro por separado.

A continuación, comprobaremos si el error de predicción actual supera el valor umbral. Y la pasada inversa se realizará solo si el error de predicción es superior al valor umbral al menos para un parámetro.

         vector<float> check = MathAbs(error) - STD * STD_Multiplier;
         if(check.Max() > 0)
           {
            //---
            Result.AssignArray(CAGrad(error) + result);
            if(!Decoder.backProp(Result, (CNet *)NULL) ||
               !Encoder.backPropGradient(GetPointer(LastEncoder), GetPointer(Gradient)))
              {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               break;
              }
           }

Cabe señalar que este planteamiento tiene varios matices a considerar. El error medio del modelo solo se considerará al realizar una pasada inversa. Por lo tanto, en este caso, el error actual afectará al error medio solo cuando se supere el valor umbral. Como resultado, los pequeños errores que ignoremos no afectarán al valor del error medio del modelo. Así, obtendremos una sobreestimación de este indicador. Esto no resulta crítico para nosotros, ya que el indicador es puramente informativo.

"El otro lado de la moneda es que, destacando solo los valores atípicos significativos, ayudaremos al modelo a poner de relieve los principales factores que afectan a determinados indicadores. Y utilizar la desviación típica móvil como punto de referencia para el valor umbral, permitirá reducir el umbral de error aceptable en el proceso de entrenamiento, lo cual ayudará a afinar más el modelo.

Al final de las iteraciones del ciclo, guardaremos los resultados del codificador en un búfer auxiliar e informaremos al usuario sobre el progreso del proceso de entrenamiento del modelo.

         Encoder.getResults(result);
         LastEncoder.AssignArray(result);
         //---
         if(GetTickCount() - ticks > 500)
           {
            double percent = (double(i - state) / ((end - state)) + iter) * 100.0 / (Iterations);
            string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Decoder", percent, Decoder.getRecentAverageError());
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

Después de pasar con éxito todas las iteraciones de nuestro sistema de ciclos de entrenamiento, como de costumbre, borraremos el campo de comentarios en el gráfico de símbolos, registraremos los resultados del entrenamiento y finalizaremos el asesor.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Decoder", Decoder.getRecentAverageError());
   ExpertRemove();
//---
  }

No se olvide de guardar los modelos entrenados y borrar la memoria en el método de desinicialización del asesor.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
     {
      Encoder.Save(FileName + "Enc.nnw", 0, 0, 0, TimeCurrent(), true);
      Decoder.Save(FileName + "Dec.nnw", Decoder.getRecentAverageError(), 0, 0, TimeCurrent(), true);
     }
   delete Result;
   delete OpenCL;
  }

2.4 Entrenamiento de la política

El siguiente paso será aprender la política de comportamiento del Agente, que se implementará en el asesor "...\Experts\GCPC\Study.mq5". Aquí también entrenaremos el modelo de codificador del estado, que es esencialmente una parte integral de nuestro modelo de Agente. Y un modelo de fijación de objetivos.

Aunque es funcionalmente posible separar la política de comportamiento del Agente y el proceso de entrenamiento del modelo de fijación de objetivos en 2 programas separados, hemos decidido combinarlos en un solo asesor. Como se verá en el algoritmo de aplicación, estos 2 procesos están estrechamente entrelazados y utilizan muchos datos comunes. En este caso, la eficacia de la división del entrenamiento del modelo en 2 procesos paralelos con una gran proporción de operaciones duplicadas resultará bastante dudosa.

En el funcionamiento de este asesor, al igual que en el asesor en interacción con el entorno, se utilizan 4 modelos, 3 de ellos entrenados en el mismo.

CNet                 Actor;
CNet                 StateEncoder;
CNet                 Encoder;
CNet                 Goal;

En el método OnInit de inicialización del asesor, cargaremos la muestra de entrenamiento, igual que en el asesor comentado anteriormente.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

A continuación, cargaremos los modelos. Primero intentaremos abrir el codificador preentrenado. Deberá entrenarse en el primer paso del algoritmo Goal-Conditioned Predictive Coding. Y la ausencia de este modelo no nos permitirá pasar a la siguiente fase.

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Cann't load Encoder model");
      return INIT_FAILED;
     }

Tras leer con éxito el modelo del Codificador, intentaremos abrir los modelos restantes. Todos ellos están formados como asesores. Por lo tanto, si se produce algún error, crearemos nuevos modelos y los inicializaremos con parámetros aleatorios.

   if(!StateEncoder.Load(FileName + "StEnc.nnw", temp, temp, temp, dtStudied, true) ||
      !Goal.Load(FileName + "Goal.nnw", temp, temp, temp, dtStudied, true) ||
      !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *goal = new CArrayObj();
      CArrayObj *encoder = new CArrayObj();
      if(!CreateDescriptions(actor, goal, encoder))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !StateEncoder.Create(encoder) || !Goal.Create(goal))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      delete actor;
      delete goal;
      delete encoder;
      //---
     }

A continuación, trasladaremos todos los modelos a un único contexto OpenCL. Para desactivar el enmascaramiento de los datos de origen, deberemos establecer el modo de entrenamiento del Codificador en false.

Como paso siguiente, verificaremos que las arquitecturas de todos los modelos cargados coincidan, para eliminar posibles errores en la transferencia de datos entre modelos.

   Actor.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }
   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("The scope of the Encoder does not match the embedding size (%d <> %d)", EmbeddingSize, Result.Total());
      return INIT_FAILED;
     }
//---
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(),
                                                                                               (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   StateEncoder.GetLayerOutput(0, Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("Input size of State Encoder doesn't match Bottleneck (%d <> %d)", Result.Total(), EmbeddingSize);
      return INIT_FAILED;
     }
//---
   StateEncoder.getResults(Result);
   int latent_state = Result.Total();
   Actor.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Actor doesn't match output State Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }
//---
   Goal.GetLayerOutput(0, Result);
   latent_state = Result.Total();
   Encoder.getResults(Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Goal doesn't match output Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }
//---
   Goal.getResults(Result);
   if(Result.Total() != NRewards)
     {
      PrintFormat("The scope of Goal doesn't match rewards count (%d <> %d)", Result.Total(), NRewards);
      return INIT_FAILED;
     }

Tras superar con éxito todos los controles necesarios, crearemos los búferes auxiliares en el contexto OpenCL utilizado.

   if(!bLastEncoder.BufferInit(EmbeddingSize, 0) ||
      !bGradient.BufferInit(MathMax(EmbeddingSize, AccountDescr), 0) ||
      !bLastEncoder.BufferCreate(OpenCL) ||
      !bGradient.BufferCreate(OpenCL))
     {
      PrintFormat("Error of create buffers: %d", GetLastError());
      return INIT_FAILED;
     }

Y generaremos un evento personalizado para iniciar el proceso de entrenamiento.

   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

En el método de desinicialización del asesor, guardaremos los modelos entrenados y eliminaremos los objetos dinámicos utilizados.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
     {
      Actor.Save(FileName + "Act.nnw", 0, 0, 0, TimeCurrent(), true);
      StateEncoder.Save(FileName + "StEnc.nnw", 0, 0, 0, TimeCurrent(), true);
      Goal.Save(FileName + "Goal.nnw", 0, 0, 0, TimeCurrent(), true);
     }
   delete Result;
   delete OpenCL;
  }

El propio proceso de entrenamiento del modelo se llevará a cabo con el método Train. En el cuerpo del método, primero generaremos un búfer de probabilidades de selección de trayectorias para entrenar los modelos. Recordemos que estamos ponderando todas las trayectorias de la muestra de entrenamiento según sus rendimientos. Y las pasadas más lucrativas tienen más probabilidades de participar en el proceso de entrenamiento.

//+------------------------------------------------------------------+
//| Train function                                                   |
//+------------------------------------------------------------------+
void Train(void)
  {
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);

A continuación, inicializaremos las variables locales. Y aquí podemos observar ya 2 vectores de desviaciones típicas, que utilizaremos para los modelos de fijación de la política y los objetivos.

   vector<float> result, target;
   matrix<float> targets;
   STD_Actor = vector<float>::Zeros(NActions);
   STD_Goal = vector<float>::Zeros(NRewards);
   int std_count = 0;
   bool Stop = false;
//---
   uint ticks = GetTickCount();

Aunque ninguno de los modelos entrenados tiene bloques y pilas recurrentes en su arquitectura, seguiremos creando un sistema de ciclos para entrenar los modelos. Al fin y al cabo, los datos de entrada para los modelos entrenados son generados por el codificador, que explota la arquitectura GPT.

En el cuerpo del ciclo exterior, muestrearemos la trayectoria y el estado inicial en ella.

   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int batch = GPTBars + 50;
      int state = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - PrecoderBars - batch));
      if(state <= 0)
        {
         iter--;
         continue;
        }

Luego borraremos la pila del Codificador y su búfer de últimos resultados.

      Encoder.Clear();
      bLastEncoder.BufferInit(EmbeddingSize, 0);

Note que utilizaremos un búfer para registrar el último estado del Codificador, aunque no planeemos pasadas directas para este modelo. Esencialmente, podemos usar para las pasadas directas un puntero al modelo, como implementamos en el asesor de interacción con el entorno. Sin embargo, al pasar a una nueva trayectoria, tendremos que poner a cero no solo la pila de estados latentes, sino también el búfer de resultados del modelo. Y esto resulta más fácil de hacer si usamos un búfer adicional.

En el cuerpo del ciclo anidado, cargaremos los datos de estado analizados de la muestra de entrenamiento y generaremos una representación comprimida de los mismos utilizando el modelo del Codificador.

      int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
      for(int i = state; i < end; i++)
        {
         bState.AssignArray(Buffer[tr].States[i].state);
         //---
         if(!bLastEncoder.BufferWrite() ||
            !Encoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bLastEncoder)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

A continuación, llenaremos el búfer de descripción del estado de la cuenta, que se complementará con los armónicos de marca temporal.

         float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
         float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
         bAccount.Clear();
         bAccount.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance);
         bAccount.Add(Buffer[tr].States[i].account[1] / PrevBalance);
         bAccount.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity);
         bAccount.Add(Buffer[tr].States[i].account[2]);
         bAccount.Add(Buffer[tr].States[i].account[3]);
         bAccount.Add(Buffer[tr].States[i].account[4] / PrevBalance);
         bAccount.Add(Buffer[tr].States[i].account[5] / PrevBalance);
         bAccount.Add(Buffer[tr].States[i].account[6] / PrevBalance);
         double time = (double)Buffer[tr].States[i].account[7];
         double x = time / (double)(D'2024.01.01' - D'2023.01.01');
         bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_MN1);
         bAccount.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_W1);
         bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_D1);
         bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         if(bAccount.GetIndex() >= 0)
            bAccount.BufferWrite();

Luego combinaremos la representación comprimida del estado del entorno analizado con un vector de descripción del estado de la cuenta.

         //--- State embedding
         if(!StateEncoder.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat*)GetPointer(bAccount)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

Y ahora, para implementar la pasada directa del Actor, necesitaremos darle un objetivo. Al igual que con el Decision Transformer, en este paso utilizaremos como punto de referencia los resultados reales obtenidos al interactuar con el entorno, y las acciones reales realizadas por el Agente como resultados del rendimiento de la política objetivo. De este modo estableceremos vínculos entre el propósito y la acción en una determinada condición del entorno. Pero aquí hay un matiz a considerar. Al entrenar el Autocodificador, pretendíamos obtener datos predictivos para varias velas por adelantado. En consecuencia, ahora esperamos disponer de información de previsión para las próximas velas en una representación concisa del estado actual. Resulta lógico suponer que las acciones del Agente en esta fase deberán estar diseñadas para producir un resultado en el periodo de tiempo previsible. Parece que podemos tomar la recompensa total a lo largo del periodo de previsión como objetivo de la acción a emprender, pero, ¿quién dice que una operación abierta en ese momento deberá cerrarse solo después de que expire el periodo de previsión? Puede cerrarse antes o después. Para el caso "posterior", no podemos mirar más allá de los valores previstos. Por lo tanto, solo podremos tomar el resultado al final del periodo de previsión. Pero cuando la dirección del movimiento de precios cambia dentro del periodo de previsión, la operación deberá cerrarse antes. Por lo tanto, nuestro objetivo potencial debería ser el valor máximo durante el periodo de previsión, considerando el factor de descuento.

El problema es que el búfer de reproducción de experiencias almacena las recompensas acumuladas hasta el final del episodio. Y nosotros, por el contrario, necesitaremos sumar las recompensas del estado analizado a lo largo del horizonte de datos previsto. Por ello, primero reconstruiremos la recompensa en cada paso sin factor de descuento.

         targets = matrix<float>::Zeros(PrecoderBars, NRewards);
         result.Assign(Buffer[tr].States[i + 1].rewards);
         for(int t = 0; t < PrecoderBars; t++)
           {
            target = result;
            result.Assign(Buffer[tr].States[i + t + 2].rewards);
            target = target - result * DiscFactor;
            targets.Row(target, t);
           }

Y luego la sumaremos en orden inverso, teniendo en cuenta el factor de descuento.

         for(int t = 1; t < PrecoderBars; t++)
           {
            target = targets.Row(t - 1) + targets.Row(t) * MathPow(DiscFactor, t);
            targets.Row(target, t);
           }

De la matriz resultante seleccionaremos la fila con la máxima recompensa, que será nuestro objetivo.

         result = targets.Sum(1);
         ulong row = result.ArgMax();
         target = targets.Row(row);
         bGoal.AssignArray(target);

Estoy bastante de acuerdo con la observación de que el beneficio (o la pérdida) que obtenemos en pasos temporales posteriores puede estar relacionado con operaciones que el Agente ha realizado antes o después. Aquí tenemos dos puntos a considerar.

La referencia a transacciones anteriores no es del todo correcta. Al fin y al cabo, el hecho de que el Agente las haya dejado abiertas es un acto del momento presente, y, por tanto, su resultado posterior será consecuencia de esa acción.

En cuanto a las acciones posteriores, en el marco del análisis de trayectorias, no estamos analizando las acciones individuales, sino la política de comportamiento del Actor en su conjunto. Por consiguiente, el objetivo también se fijará para la política en un futuro previsible, en lugar de una acción individual. Y desde este punto de vista, fijar un objetivo máximo para el periodo de previsión resultará bastante pertinente.

Dado el objetivo preparado, tenemos datos suficientes para hacer una pasada directa del Actor.

         //--- Actor
         if(!Actor.feedForward((CNet *)GetPointer(StateEncoder), -1, (CBufferFloat*)GetPointer(bGoal)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

A continuación, tendremos que ajustar los parámetros del modelo para minimizar el error entre las acciones previstas y las realmente realizadas durante la interacción con el entorno. Aquí utilizaremos métodos de aprendizaje supervisado, que complementaremos haciendo hincapié en las desviaciones máximas. Al igual que en el algoritmo descrito anteriormente, primero calcularemos el error cuadrático medio móvil de las predicciones para cada parámetro.

         target.Assign(Buffer[tr].States[i].action);
         target.Clip(0, 1);
         Actor.getResults(result);
         vector<float> error = target - result;
         std_count = MathMin(std_count, 999);
         STD_Actor = MathSqrt((MathPow(STD_Actor, 2) * std_count + MathPow(error, 2)) / (std_count + 1));

Y compararemos el error actual con el valor umbral. La pasada inversa se realizará solo si existe una desviación por encima del umbral para al menos un parámetro.

         check = MathAbs(error) - STD_Actor * STD_Multiplier;
         if(check.Max() > 0)
           {
            Result.AssignArray(CAGrad(error) + result);
            if(!Actor.backProp(Result, (CBufferFloat *)GetPointer(bGoal), (CBufferFloat *)GetPointer(bGradient)) ||
               !StateEncoder.backPropGradient(GetPointer(bAccount), (CBufferFloat *)GetPointer(bGradient)))
              {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               Stop = true;
               break;
              }
           }

Tras actualizar los parámetros del Actor, pasaremos a entrenar el modelo de fijación de objetivos. A diferencia del Actor, solo utilizaremos como datos de entrada una representación comprimida del estado analizado obtenida del Codificador. Y no necesitaremos preparar datos adicionales antes de aplicar una pasada directa.

         //--- Goal
         if(!Goal.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

Para la política del Actor, utilizaremos los objetivos anteriores como valores objetivo del entrenamiento del modelo. Pero con un pequeño añadido. En muchos trabajos se recomienda utilizar un factor de incremento de los resultados reales obtenidos a la hora de establecer los objetivos para las políticas entrenadas. Esto debería animar a las políticas de comportamiento a elegir mejores acciones. Por otra parte, enseñaremos directamente el modelo de fijación de objetivos para obtener mejores resultados. Para ello, multiplicaremos por 2 las recompensas reales al formar el vector de valores objetivo. Pero aquí hay, como siempre, un matiz interesante: no podemos simplemente multiplicar el vector de recompensas reales por 2, dado que entre las recompensas obtenidas puede haber valores negativos, y multiplicarlos por 2 solo empeorará las expectativas. Por lo tanto, primero determinaremos el signo de la recompensa.

         target=targets.Row(row);
         result = target / (MathAbs(target) + FLT_EPSILON);

Como resultado de esta operación, esperamos obtener un vector que contenga "-1" para los valores negativos y "1" para los valores positivos. Elevando el vector "2" al grado del vector resultante, obtendremos "2" para valores positivos y "½" para valores negativos.

        result = MathPow(vector<float>::Full(NRewards, 2), result);

Ahora podremos multiplicar el vector de resultados reales por el vector de coeficientes obtenido anteriormente para duplicar la recompensa esperada. Eso utilizaremos como valores objetivo para entrenar nuestro modelo de fijación de objetivos.

         target = target * result;
         Goal.getResults(result);
         error = target - result;
         std_count = MathMin(std_count, 999);
         STD_Goal = MathSqrt((MathPow(STD_Goal, 2) * std_count + MathPow(error, 2)) / (std_count + 1));
         std_count++;
         check = MathAbs(error) - STD_Goal * STD_Multiplier;
         if(check.Max() > 0)
           {
            Result.AssignArray(CAGrad(error) + result);
            if(!Goal.backProp(Result, (CBufferFloat *)NULL, (CBufferFloat *)NULL))
              {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               Stop = true;
               break;
              }
           }

Aquí también explotaremos la idea de utilizar expresiones de forma cerrada para optimizar el modelo centrándonos en las desviaciones máximas.

En esta fase, optimizaremos los parámetros de todos los modelos entrenados. Luego guardaremos los resultados del funcionamiento del Codificador en el búfer correspondiente,

         Encoder.getResults(result);
         bLastEncoder.AssignArray(result);

informaremos al usuario sobre el progreso del proceso de entrenamiento y pasaremos a la siguiente iteración del sistema de ciclos.

         //---
         if(GetTickCount() - ticks > 500)
           {
            double percent = (double(i - state) / ((end - state)) + iter) * 100.0 / (Iterations);
            string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Actor", percent, Actor.getRecentAverageError());
            str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Goal", percent, Goal.getRecentAverageError());
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

Una vez completadas todas las iteraciones del sistema de ciclos de entrenamiento del modelo, borraremos el campo de comentarios del gráfico del instrumento. Luego registraremos los resultados del entrenamiento y finalizaremos el asesor.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Goal", Goal.getRecentAverageError());
   ExpertRemove();
//---
  }

Con esto concluiremos la descripción del algoritmo de los programas utilizados. En el archivo adjunto, podrá ver el código completo de todos los programas utilizados en el artículo. Ahí también encontrará un asesor para probar los modelos entrenados; no nos detendremos en él ahora.


3. Simulación

Arriba hemos hecho bastante trabajo para implementar el método Goal-Conditioned Predictive Coding usando MQL5. El tamaño de este artículo confirma la cantidad de trabajo realizado. Es hora de comprobar sus resultados.

Como es habitual, el entrenamiento y las pruebas de los modelos los hemos realizado con los datos históricos de EURUSD, marco temporal H1. Los modelos se han entrenado con los datos históricos de los 7 primeros meses de 2023, mientras que las pruebas de los modelos entrenados se han realizado con los datos de agosto de 2023, que siguen inmediatamente al periodo histórico de entrenamiento.

El entrenamiento se ha efectuado de forma iterativa. En primer lugar, hemos corregido la muestra de entrenamiento, que hemos recogido en 2 fases. En el primer paso, hemos guardado las pasadas de los datos de la señal real en la muestra de entrenamiento, como se propone en el método Real-ORL. Y luego hemos complementado la muestra de entrenamiento con pasadas utilizando el asesor "...\Experts\GCPC\Research.mq5" y políticas aleatorias.

Hemos entrenado el Autocodificador con estos datos utilizando el asesor "...\Experts\GCPC\StudyEncoder.mq5". Como ya hemos mencionado, en lo que respecta al entrenamiento de este asesor, todas las pasadas son idénticas. Y el entrenamiento del modelo no requiere una actualización adicional de la muestra de entrenamiento. Por lo tanto, hemos entrenado el Autocodificador enmascarado hasta obtener resultados aceptables.

La segunda etapa consiste en entrenar la política de comportamiento del Agente y el modelo de fijación de objetivos. Aquí ya hemos utilizado un enfoque iterativo con el entrenamiento del modelo, así como la posterior actualización de los datos de entrenamiento. Tengo que decir que esta etapa me ha sorprendido gratamente. El proceso de entrenamiento ha resultado bastante estable y con una buena dinámica de resultados. El proceso de entrenamiento ha producido una política capaz de generar beneficios tanto en el horizonte temporal de entrenamiento como en el de prueba.


Conclusión

En este artículo, nos hemos familiarizado con un método bastante interesante: el Goal-Conditioned Predictive Coding, cuya principal aportación consiste en dividir el proceso de entrenamiento del modelo en 2 subprocesos: el aprendizaje de la trayectoria y el aprendizaje separado de la política. El aprendizaje de la trayectoria hace hincapié en la posibilidad de proyectar las tendencias observadas sobre los estados futuros, lo que generalmente aumenta la informatividad de los datos retornados al Agente para la toma de decisiones.

En la parte práctica de este artículo, hemos implementado nuestra visión del método propuesto utilizando MQL5 y hemos confirmado la eficacia del enfoque en la práctica.

Una vez más, quiero señalar que todo el material presentado en este artículo supone una demostración de las capacidades analizadas, y en ningún caso está preparado para su uso en el mundo real de los mercados financieros.


Enlaces


Programas usados en el artículo

# Nombre Tipo Descripción
1 Research.mq5 Asesor Asesor de recopilación de datos
2 ResearchRealORL.mq5
Asesor
Asesor de recopilación de ejemplos con el método Real-ORL
3 Study.mq5  Asesor Asesor de entrenamiento de la política
4 StudyEncoder.mq5 Asesor
Asesor de entrenamiento del Autocodificador
5 Test.mq5 Asesor Asesor para la prueba de modelos
6 Trajectory.mqh Biblioteca de clases Estructura de descripción del estado del sistema.
7 NeuroNet.mqh Biblioteca de clases Biblioteca de clases para crear una red neuronal
8 NeuroNet.cl Biblioteca Biblioteca de código de programa OpenCL

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

Archivos adjuntos |
MQL5.zip (757.07 KB)
Algoritmos de optimización de la población: Algoritmo híbrido de optimización de forrajeo bacteriano con algoritmo genético (Bacterial Foraging Optimization - Genetic Algorithm, BFO-GA) Algoritmos de optimización de la población: Algoritmo híbrido de optimización de forrajeo bacteriano con algoritmo genético (Bacterial Foraging Optimization - Genetic Algorithm, BFO-GA)
Este artículo presenta un nuevo enfoque para resolver problemas de optimización combinando las ideas de los algoritmos de optimización de forrajeo bacteriano (BFO) y las técnicas utilizadas en el algoritmo genético (GA) en un algoritmo híbrido BFO-GA. Dicha técnica utiliza enjambres bacterianos para buscar una solución óptima de manera global y operadores genéticos para refinar los óptimos locales. A diferencia del BFO original, ahora las bacterias pueden mutar y heredar genes.
Redes neuronales: así de sencillo (Parte 70): Mejoramos las políticas usando operadores de forma cerrada (CFPI) Redes neuronales: así de sencillo (Parte 70): Mejoramos las políticas usando operadores de forma cerrada (CFPI)
En este trabajo, proponemos introducir un algoritmo que use operadores de mejora de políticas de forma cerrada para optimizar las acciones offline del Agente.
Aprendiendo MQL5 de principiante a profesional (Parte II): Tipos de datos básicos y uso de variables Aprendiendo MQL5 de principiante a profesional (Parte II): Tipos de datos básicos y uso de variables
Continuamos la serie para principiantes. Hoy veremos cómo crear constantes y variables, además de registrar la fecha, los colores y otros datos útiles. Asimismo, aprenderemos a crear enumeraciones como días de la semana o estilos de cadena (sólido, punteado, etc.). Las variables y las expresiones son la base de la programación: se encuentran necesariamente en el 99% de los programas, por lo que comprenderlas es fundamental. Y así, si es usted nuevo en el mundo de la programación, este es un buen comienzo. Nivel de conocimientos de programación: muy básico, dentro del ámbito de mi artículo anterior (el enlace está al principio).
GIT: ¿Pero qué es esto? GIT: ¿Pero qué es esto?
En este artículo presentaré una herramienta de suma importancia para quienes desarrollan programas. Si no conoces GIT, consulta este artículo para tener una noción de lo que se trata esta herramienta y cómo usarla junto al MQL5.