English Русский Deutsch 日本語 Português
preview
Redes neuronales: así de sencillo (Parte 76): Exploración de diversos patrones de interacción con Multi-future Transformer

Redes neuronales: así de sencillo (Parte 76): Exploración de diversos patrones de interacción con Multi-future Transformer

MetaTrader 5Sistemas comerciales | 5 agosto 2024, 10:52
116 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

Predecir los próximos movimientos de los precios es uno de los requisitos clave para construir una estrategia de negociación con éxito. Por lo tanto, tenemos que explorar las posibilidades de crear previsiones precisas y multimodales del movimiento futuro. En artículos anteriores, nos familiarizamos con algunos métodos para predecir los movimientos de los precios. Entre ellas, las multimodales, que ofrecían diversas variantes de desarrollo del evento.

Sin embargo, todos ellos rara vez se centran en las posibilidades futuras de interacción entre los agentes analizados, lo que puede llevar a una pérdida de información y a una previsión subóptima. Además, es bastante difícil escalar los métodos discutidos anteriormente en caso de que tengamos múltiples agentes, ya que las predicciones independientes pueden dar lugar a un número exponencial de combinaciones. La mayoría de las combinaciones resultantes no son viables debido a predicciones contradictorias. Por ello, es importante centrarse en la predicción de escenas en su conjunto, estimando el estado futuro de múltiples agentes simultáneamente.

Los autores del artículo "Multi-future Transformer: Learning diverse interaction modes for behavior prediction in autonomous driving" sugieren utilizar el método Multi-future Transformer (MFT) para resolver este tipo de problemas. Su idea principal es descomponer la distribución multimodal del futuro en varias distribuciones unimodales, lo que permite simular eficazmente varios modelos de interacción entre agentes en la escena.

En la MFT, las previsiones son generadas por una red neuronal con parámetros fijos en una sola pasada, sin necesidad de muestrear estocásticamente variables latentes, predeterminar anclajes o ejecutar un algoritmo iterativo de postprocesamiento. Esto permite que el modelo funcione de forma determinista y repetible.


1. Algoritmo Transformador Multifuturo

El objetivo principal del método MFT (Multi-future Transformer), es predecir de forma consistente el movimiento Y futuro de todos los agentes de la escena. Para ello, analiza el estado dinámico de los agentes X y la información contextual M. Así, la distribución de probabilidad global a capturar es P(Y|X,M), que es multimodal como resultado de la clara evolución de la escena.

Suele ser muy difícil modelizar directamente la distribución multimodal conjunta. Los autores del método introducen el supuesto de que la distribución objetivo puede descomponerse en una mezcla de varias distribuciones unimodales y, a continuación, estas distribuciones unimodales se modelan por separado, lo que se formula como

donde Ik representa el k-ésimo componente de modo;
p(Ik|X,M) es la distribución de probabilidad de los distintos modos;
      p(Y|X,M,Ik) representa la distribución unimodal factorizada.

Bajo esta formulación, el punto clave en la MFT es que la principal diferencia entre cada modo de distribución objetivo reside en los diferentes modelos de interacción entre agentes, y entre agentes y contexto. El modelado de cada distribución unimodal puede realizarse estudiando los patrones de las interacciones modo-correspondencia. Intuitivamente, la incertidumbre de la escena futura puede descomponerse principalmente en dos partes: incertidumbre de intención e incertidumbre de interacción.

Las intenciones no observables pueden captarse bien modelando la interacción entre el agente y el contexto, ya que el punto final de la trayectoria futura, que representa las intenciones del agente, está estrechamente relacionado con el contexto de la escena. Para tener en cuenta la incertidumbre de la interacción, se modelan distintos modos de interacción entre los agentes. Así, al capturar conjuntamente las interacciones entre agentes y entre agentes y contextos, se minimiza la incertidumbre futura y se puede determinar la evolución de una escena.

Para lograr la descomposición de la distribución conjunta multimodal descrita anteriormente, se diseñó la arquitectura general del enfoque MFT propuesto. Está compuesto por tres partes:

  • Codificadores
  • Módulo de interacción paralela
  • Cabeceras de predicción

El modelo incluye dos tipos de codificadores:

  • El codificador de agentes dinámicos se utiliza para extraer características de los estados dinámicos observados,
  • El codificador de segmentos contextuales sirve como visor de mapas para aprender funciones puntuales para puntos de franja.

El núcleo del modelo MFT es un módulo de interacción paralela, que consta de varios bloques de interacción en una estructura paralela y estudia las características futuras del movimiento de los agentes para cada modo. Las tres cabeceras de predicción incluyen:

  • Descodificador de movimiento,
  • Descodificador de puntuación de agentes,
  • Descodificador de puntuación de escenas.

Se encargan de descodificar las trayectorias futuras de cada agente y de estimar las puntuaciones de confianza para cada trayectoria prevista y cada modo de escena. En esta arquitectura, los caminos por los que pasan las señales feed-forward y la retropropagación de cada modo son independientes entre sí, y cada camino contiene un bloque de interacción único que proporciona interacción de información entre señales del mismo modo. Por lo tanto, las unidades de interacción pueden captar simultáneamente los patrones de interacción correspondientes a los distintos modos. Sin embargo, los codificadores y las cabeceras de predicción son comunes a cada modo, mientras que los bloques de interacción se parametrizan como objetos diferentes. Por lo tanto, cada distribución unimodal, que teóricamente tiene parámetros diferentes, puede modelarse de una forma más eficiente en cuanto a parámetros. A continuación se muestra la visualización original del método.

Las trayectorias observadas de los agentes en el horizonte histórico pueden representarse como X={x1,.... ,xatt}, donde el estado dinámico en cada paso temporal xt=(x,y,vx,vy, w) contiene la posición (x,y), la velocidad (vx,vy), y el ángulo de viraje w en ese momento.

Los estados dinámicos de un agente se consideran como un conjunto de puntos en un espacio multidimensional, donde cada punto está representado por coordenadas (x,y), y otras dimensiones representan características locales adicionales. Para estos puntos, se conservan las propiedades de interacción y la estructura local dada, ya que los estados observados del agente interactúan entre sí y juntos forman la trayectoria observada del agente. Además, otra propiedad importante de estos puntos de trayectoria es el orden temporal, que es una característica implícita en comparación con los puntos de trayectoria estáticos. Naturalmente, los mismos puntos de trayectoria con un orden temporal inverso conducen a una trayectoria observada completamente distinta. Las propiedades anteriores exigen que el modelo represente los puntos de la trayectoria no sólo combinando características puntuales, sino también introduciendo información de orden temporal. Para ello, se utiliza la codificación posicional absoluta para codificar la información de ordenación. El modelo calcula las características dinámicas de cada agente del siguiente modo:

Se apilan varias capas codificadoras para mejorar la capacidad del modelo de representar información dinámica. El promediado se realiza sobre todos los puntos de la trayectoria de un agente para resumir la información dinámica histórica relacionada con un agente específico.

También se extrajeron características puntuales correspondientes al momento actual para obtener información dinámica. Esto proporciona un rendimiento similar al de la piscina media. Cuando se trata de estados históricos parcialmente observables, un marco basado en la autoatención puede simplemente enmascarar las posiciones no observables sin esfuerzo adicional, evitando así eficazmente la confusión de características causada por el relleno no válido.

Una característica distintiva de la tarea de predicción del comportamiento es la multimodalidad causada por la incertidumbre de la escena futura. Un enfoque habitual consiste en utilizar varias cabeceras de resultados para descodificar de forma independiente las trayectorias futuras de cada agente basándose en un vector de características común. Sin embargo, este método tiene dos desventajas principales:

  • La información de movimiento de varias trayectorias futuras posibles está contenida en un vector de características con una dimensión fija, lo que conduce a una información limitada y limita significativamente las capacidades expresivas del modelo;
  • Las señales de avance y retroceso de los distintos modos se mezclan mediante la interacción de características y la propagación de gradientes, lo que provoca el problema de confusión de modos que degrada la capacidad de realizar predicciones multimodales.

En cambio, en MFT, el modelado de la interacción se divide en diferentes modos para lograr un resultado multimodal mediante el aprendizaje del patrón de interacción único correspondiente a cada modo. Para ello, se diseñó un módulo de interacción paralelo. Este módulo contiene varios bloques de interacción paralela, cada uno de los cuales representa un determinado modo de desarrollo futuro de los acontecimientos. En esta estructura, las señales de avance y retroceso intra-modo atraviesan el bloque de interacción del modo correspondiente. Al mismo tiempo, los demás modos no interfieren, lo que evita el problema de la confusión de modos.

Además, cada bloque de interacción se parametriza como un objeto independiente, lo que permite que el modelo tenga suficiente poder expresivo para hacer frente a la gran variación entre los distintos modos. Cada bloque de interacción utiliza un mecanismo Self-Attention para modelar la interacción entre agentes y el mecanismo "Cross-Attention" para capturar la interacción entre agentes y la escena. Estos dos tipos de características de interacción se añaden a la característica dinámica a través de la conexión residual para obtener la función final de movimiento próximo para cada agente y modo. El proceso anterior puede describirse del siguiente modo:

La estructura final del módulo de interacción paralela puede verse como una paralelización del decodificador "Transformer" con una estructura en serie. Sin embargo, la predicción multimodal y la mayor capacidad expresiva se consiguen con un coste computacional similar al de su homólogo estándar. Los experimentos realizados por los autores del método confirman que, en combinación con el uso de la estrategia de pérdidas del tipo "el ganador se lo lleva todo" a nivel de escena, el método propuesto puede proporcionar una predicción coherente del comportamiento de varios agentes. También se realizó un estudio para demostrar la indispensabilidad de los dos tipos de interacciones modeladas y la razonabilidad del diseño de la estructura del módulo.

A continuación se presenta la visualización de los autores de la estructura del módulo.

Visualización de los autores de la estructura de bloques

Además de predecir la trayectoria futura de cada agente, MFT ejecuta una estimación de probabilidad para cada posible desarrollo de la escena, así como para cada trayectoria predicha del agente. Para las tareas de planificación del movimiento, la entrada ideal sería una predicción multimodal de la escena con una puntuación de confianza adecuada para cada modo de escena. El algoritmo de planificación puede calcular directamente la posible trayectoria de movimiento futura considerando simultáneamente todos los agentes vecinos. Para ello, los autores del método desarrollaron cabezales de predicción de varias escenas y sus probabilidades.

Para estimar la probabilidad a nivel de escena, se promedian los vectores de características de todos los agentes de la escena. Esto nos permite obtener una visión a nivel de escena.

Además, para prestar especial atención al agente, la estimación de la probabilidad debe realizarse desde el punto de vista del propio agente. En consecuencia, se crea una tercera cabecera de modelo para descodificar las estimaciones de confianza de las trayectorias previstas de cada agente. El proceso de descodificación del transformador multifuturo se formula del siguiente modo:

Obsérvese que para descodificar una escena multimodal se utiliza un descodificador con los mismos parámetros para todos los escenarios posibles.


2. Implementación en MQL5

Tras considerar los aspectos teóricos del método transformador multifuturo, pasemos a la parte práctica de nuestro artículo. Veamos cómo implementar el algoritmo en MQL5.

De la descripción anterior del algoritmo transformador multifuturo, podemos decir que la principal dificultad de implementación para nosotros es el módulo de interacción paralela. Nuestra biblioteca ya ha implementado previamente las capas multicabezal Self-Attention (CNeuronMLMHAttentionOCL) y Cross-Attention (CNeuronMH2AttentionOCL). Sin embargo, aunque se utilicen varias cabezas de atención, los flujos de datos en ellas se mezclan durante los pases hacia delante y hacia atrás. Esto no satisface las condiciones del método transformador multifuturo.

Por lo tanto, comenzaremos nuestra implementación del método creando una clase de una nueva capa neuronal.

2.1. Módulo de interacción paralela

Implementamos el algoritmo del módulo de interacción paralela en la clase CNeuronMFTOCL, que heredamos de CNeuronMLMHAttentionOCL.

Esta clase fue elegida como padre a propósito. CNeuronMLMHAttentionOCL ya contiene una funcionalidad implementada del algoritmo multicapa Self-Attention. Utilizamos el código existente para implementar la nueva clase. Pero en lugar de calcular secuencialmente las capas del bloque de atención, predeciremos varios escenarios. Además, añadiremos la funcionalidad Cross-Attention que proporciona el algoritmo MFT (Multi-future Transformer).

A continuación se muestra la estructura de la nueva clase. Como puedes ver, además de redefinir los métodos principales, estamos añadiendo otra colección de búferes de datos para la transposición de datos, que necesitaremos para implementar el mecanismo Cross-Attention.

class CNeuronMFTOCL  : public CNeuronMLMHAttentionOCL
  {
protected:
   //---
   CCollection       cTranspose;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   virtual bool      Transpose(CBufferFloat *in, CBufferFloat *out, int rows, int cols);
   virtual bool      MHCA(CBufferFloat *q, CBufferFloat *kv, 
                          CBufferFloat *score, CBufferFloat *out);
   //---
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
   virtual bool      MHCAInsideGradients(CBufferFloat *q, CBufferFloat *qg, 
                                         CBufferFloat *kv, CBufferFloat *kvg,
                                         CBufferFloat *score, CBufferFloat *aog);

public:
                     CNeuronMFTOCL(void) {};
                    ~CNeuronMFTOCL(void) {};
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint heads,
                          uint units_count, uint features,
                          ENUM_OPTIMIZATION optimization_type,
                          uint batch);
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer);
   //---
   virtual int       Type(void)   const   {  return defNeuronMFTOCL;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   virtual CLayerDescription* GetLayerInfo(void);
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
   virtual void      SetOpenCL(COpenCLMy *obj);
  };

Declarar una nueva colección de búferes de datos como estática nos permite dejar el constructor y el destructor de la clase "vacíos". Inicializamos la clase y los objetos internos en el método Init.

bool CNeuronMFTOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                         uint window, uint window_key, uint heads, uint units_count, 
                         uint features, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * features, 
                                                                optimization_type, batch))
      return false;

En los parámetros, el método recibe toda la información necesaria para recrear la arquitectura requerida. En el cuerpo del método, llamamos al método relevante de la clase base de la capa neuronal CNeuronBaseOCL.

Tenga en cuenta que no estamos llamando al método de inicialización de la clase padre directa CNeuronMLMHAttentionOCL. En su lugar, nos dirigimos a la clase base de las capas neuronales CNeuronBaseOCL. Esto se debe a algunas diferencias en la implementación de CNeuronMFTOCL y CNeuronMLMHAttentionOCL.

En la clase base CNeuronMLMHAttentionOCL procesamos secuencialmente los datos de origen en varias capas del bloque de atención y, al final, obtuvimos un resultado con una dimensión similar a la de los datos de origen. Ahora, en la nueva capa CNeuronMFTOCL, generaremos sistemáticamente varios patrones para posibles desarrollos. En consecuencia, el resultado de la capa será un múltiplo (por el número de opciones de previsión) del tamaño de los datos originales. Por lo tanto, tenemos que aumentar el búfer de resultados.

Además, para evitar la copia innecesaria de datos, en la clase base sustituimos el buffer de resultados y gradientes de la propia capa y de la última capa interna. En la implementación de la nueva clase, este enfoque es inaceptable, ya que tenemos que concatenar los resultados de varios bloques paralelos en un único búfer.

Volvamos a nuestro método de inicialización. Tras ejecutar con éxito el método de inicialización de la clase base de la capa neuronal, guardamos los parámetros clave de la arquitectura en variables internas de la clase.

   iWindow = fmax(window, 1);
   iWindowKey = fmax(window_key, 1);
   iUnits = fmax(units_count, 1);
   iHeads = fmax(heads, 1);
   iLayers = fmax(features, 1);

Tenga en cuenta que guardamos todos los parámetros en variables de la clase base. Aquí tenemos una ligera desviación entre el nombre de la variable y su funcionalidad: la variable iLayers almacenará el número de opciones previstas. Para utilizar los recursos de forma más eficiente, decidí no crear una variable adicional e "ignorar" la discrepancia entre el nombre y la funcionalidad de la variable.

A continuación, calcularemos los principales parámetros de nuestros bloques.

//--- MHSA
   uint num = 3 * iWindowKey * iHeads * iUnits;       //Size of QKV tensor
   uint qkv_weights = 3 * (iWindow + 1) * iWindowKey * iHeads; 
                                                      //Size of weights' matrix of QKV tensor
   uint scores = iUnits * iUnits * iHeads;            //Size of Score tensor
   uint mh_out = iWindowKey * iHeads * iUnits;        //Size of multi-heads self-attention
   uint out = iWindow * iUnits;                       //Size of output tensor
   uint w0 = (iWindowKey + 1) * iHeads * iWindow;     //Size W0 tensor
//--- MHCA
   uint num_q = iWindowKey * iHeads * iUnits;         //Size of Q tensor
   uint num_kv = 2 * iWindowKey * iHeads * iUnits;    //Size of KV tensor
   uint q_weights = (iWindow + 1) * iWindowKey * iHeads; 
                                                      //Size of weights' matrix of Q tensor
   uint kv_weights = 2 * (iUnits + 1) * iWindowKey * iHeads; 
                                                      //Size of weights' matrix of KV tensor
   uint scores_ca = iUnits * iWindow * iHeads;            //Size of Score tensor
//--- FF
   uint ff_1 = 4 * (iWindow + 1) * iWindow;    //Size of weights' matrix 1-st feed forward layer
   uint ff_2 = (4 * iWindow + 1) * iWindow;    //Size of weights' matrix 2-nd feed forward layer

Tras completar el trabajo preparatorio, organizaremos un bucle en el que crearemos secuencialmente búferes de datos para cada escenario de nuestro bloque de interacción paralelo.

   for(uint i = 0; i < iLayers; i++)
     {
      CBufferFloat *temp = NULL;
      for(int d = 0; d < 2; d++)
        {
         //--- MHSA
         //--- Initilize QKV tensor
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(num, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!QKV_Tensors.Add(temp))
            return false;

Aquí crearemos un bucle anidado para crear buffers de paso hacia delante y hacia atrás. En el cuerpo del bucle anidado, primero creamos un búfer de Query, Key y Value incrustados concatenados del bloque MHSA. También añadimos un búfer para la matriz "Score".

         //--- Initialize scores
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(scores, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!S_Tensors.Add(temp))
            return false;

Añade un búfer de resultados de atención múltiple.

         //--- Initialize multi-heads attention out
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(mh_out, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!AO_Tensors.Add(temp))
            return false;

A la salida del bloque MHSA, creamos un buffer para combinar los resultados de diferentes cabezas de atención.

         //--- Initialize attention out
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(out, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!FF_Tensors.Add(temp))
            return false;

Cabe señalar aquí que para satisfacer los requisitos del algoritmo MFT, los resultados de los cabezales de atención se combinan sólo dentro de un modo de interacción.

A continuación creamos buffers similares para el bloque Cross-Attention (MHCA). Sin embargo, ahora separamos el búfer de incrustación Query de Key y Value, ya que se utilizarán datos de origen diferentes.

         //--- MHCA
         //--- Initilize QKV tensor
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(num_q, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!QKV_Tensors.Add(temp))
            return false;
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(out, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!cTranspose.Add(temp))
            return false;
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(num_kv, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!QKV_Tensors.Add(temp))
            return false;
         //--- Initialize scores
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(scores_ca, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!S_Tensors.Add(temp))
            return false;
         //--- Initialize multi-heads cross attention out
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(mh_out, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!AO_Tensors.Add(temp))
            return false;
         //--- Initialize attention out
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(out, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!FF_Tensors.Add(temp))
            return false;

Después de crear los buffers para los bloques de atención, creamos los buffers del bloque FeedForward.

         //--- Initialize Feed Forward 1
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(4 * out, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!FF_Tensors.Add(temp))
            return false;
         //--- Initialize Feed Forward 2
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(out, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!FF_Tensors.Add(temp))
            return false;
        }

Hemos creado buffers con los resultados y los gradientes de error de los distintos bloques. A continuación, tenemos que crear matrices de pesos entrenables para estos bloques. Los creamos en la misma secuencia. En primer lugar, generamos pesos para las incrustaciones Query, Key y Value del bloque MHSA.

      //--- MHSA
      //--- Initilize QKV weights
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         return false;
      if(!temp.Reserve(qkv_weights))
         return false;
      float k = (float)(1 / sqrt(iWindow + 1));
      for(uint w = 0; w < qkv_weights; w++)
        {
         if(!temp.Add((GenerateWeight() - 0.5f)* k))
            return false;
        }
      if(!temp.BufferCreate(OpenCL))
         return false;
      if(!QKV_Weights.Add(temp))
         return false;

Y una capa para combinar cabezales de atención.

      //--- Initilize Weights0
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         return false;
      if(!temp.Reserve(w0))
         return false;
      for(uint w = 0; w < w0; w++)
        {
         if(!temp.Add((GenerateWeight() - 0.5f)* k))
            return false;
        }
      if(!temp.BufferCreate(OpenCL))
         return false;
      if(!FF_Weights.Add(temp))
         return false;

Repita las operaciones para el bloque MHCA.

      //--- MHCA
      //--- Initilize Q weights
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         return false;
      if(!temp.Reserve(q_weights))
         return false;
      for(uint w = 0; w < q_weights; w++)
        {
         if(!temp.Add((GenerateWeight() - 0.5f)* k))
            return false;
        }
      if(!temp.BufferCreate(OpenCL))
         return false;
      if(!QKV_Weights.Add(temp))
         return false;
      //--- Initilize KV weights
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         return false;
      if(!temp.Reserve(kv_weights))
         return false;
      float kv = (float)(1 / sqrt(iUnits + 1));
      for(uint w = 0; w < kv_weights; w++)
        {
         if(!temp.Add((GenerateWeight() - 0.5f)* kv))
            return false;
        }
      if(!temp.BufferCreate(OpenCL))
         return false;
      if(!QKV_Weights.Add(temp))
         return false;
      //--- Initilize Weights0
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         return false;
      if(!temp.Reserve(w0))
         return false;
      for(uint w = 0; w < w0; w++)
        {
         if(!temp.Add((GenerateWeight() - 0.5f)* k))
            return false;
        }
      if(!temp.BufferCreate(OpenCL))
         return false;
      if(!FF_Weights.Add(temp))
         return false;

Crear matrices para el bloque FeedForward.

      //--- Initilize FF Weights
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         return false;
      if(!temp.Reserve(ff_1))
         return false;
      for(uint w = 0; w < ff_1; w++)
        {
         if(!temp.Add((GenerateWeight() - 0.5f)* k))
            return false;
        }
      if(!temp.BufferCreate(OpenCL))
         return false;
      if(!FF_Weights.Add(temp))
         return false;
      //---
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         return false;
      if(!temp.Reserve(ff_2))
         return false;
      k = (float)(1 / sqrt(4 * iWindow + 1));
      for(uint w = 0; w < ff_2; w++)
        {
         if(!temp.Add((GenerateWeight() - 0.5f)* k))
            return false;
        }
      if(!temp.BufferCreate(OpenCL))
         return false;
      if(!FF_Weights.Add(temp))
         return false;

Después implementamos otro bucle anidado, en el que creamos buffers para actualizar las matrices de pesos. Tenga en cuenta que el número de buffers de momentos depende del algoritmo de optimización elegido.

      for(int d = 0; d < (optimization == SGD ? 1 : 2); d++)
        {
         //--- MHSA
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(qkv_weights, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!QKV_Weights.Add(temp))
            return false;
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(w0, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!FF_Weights.Add(temp))
            return false;
         //--- MHCA
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(q_weights, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!QKV_Weights.Add(temp))
            return false;
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(kv_weights, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!QKV_Weights.Add(temp))
            return false;
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(w0, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!FF_Weights.Add(temp))
            return false;
         //--- FF Weights momentus
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(ff_1, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!FF_Weights.Add(temp))
            return false;
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            return false;
         if(!temp.BufferInit(ff_2, 0))
            return false;
         if(!temp.BufferCreate(OpenCL))
            return false;
         if(!FF_Weights.Add(temp))
            return false;
        }
     }
//---
   return true;
  }

Después de inicializar con éxito todos los búferes de datos necesarios, salimos del método con el resultado true.

Hemos creado muchos búferes. Para no confundirnos, vamos a crear una tabla de navegación de búfer de datos.

ID QKV_Tensors S_Tensors, 
AO_Tensors
FF_Tensors QKV_Weights FF_Weights
0 Query, Key, Value MHSA MHSA MHSA Out Query, Key, Value MHSA
MHSA W0
1 Query MHCA MHCA MHCA Out Query MHCA
MHCA W0
2 Key, Value MHCA
Gradient MHSA
FF1 Key, Value MHCA
FF1
3 Gradient
Query, Key, Value MHSA

Gradient MHCA FF2 Momentum1
Query, Key, Value MHSA
FF2
4 Gradient Query MHCA

Gradient MHSA Out Momentum1 Query MHCA
Momentum1 MHSA W0
5 Gradient Key, Value MHCA

Gradient MHCA Out Momentum1 Key, Value MHCA
Momentum1 MHCA W0
6

Gradient FF1
Momentum2
Query, Key, Value MHSA

Momentum1 FF1
7

Gradient FF2
Momentum2 Query MHCA
Momentum1 FF2
8


Momentum2 Key, Value MHCA
Momentum2 MHSA W0
9



Momentum2 MHCA W0
10



Momentum2 FF1
11         Momentum2 FF2

Tras la inicialización de la clase CNeuronMFTOCL, pasamos a construir un algoritmo feed-forward. Al implementar el algoritmo MFT, no creamos nuevos núcleos en el lado del programa OpenCL. Sin embargo, para llamar a los núcleos creados previamente, tuvimos que crear algunos métodos en el lado del programa principal. En particular, para nuestra clase CNeuronMFTOCL, hemos creado un método de transposición de matrices. El algoritmo del método es similar al "feed-forward" del layer de transposición. Sin embargo, hay diferencias en los detalles. En los parámetros, el nuevo método recibe punteros a 2 búferes de datos (datos de origen y resultados), así como los tamaños de la matriz original.

bool CNeuronMFTOCL::Transpose(CBufferFloat *in, CBufferFloat *out, int rows, int cols)
  {
   if(!in || !out)
      return false;
//---
   uint global_work_offset[2] = {0, 0};
   uint global_work_size[2] = {rows, cols};
   if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_in, in.GetIndex()))
      return false;
   if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_out, out.GetIndex()))
      return false;
   if(!OpenCL.Execute(def_k_Transpose, 2, global_work_offset, global_work_size))
     {
      string error;
      CLGetInfoString(OpenCL.GetContext(), CL_ERROR_DESCRIPTION, error);
      printf("%s %d Error of execution kernel Transpose: %d -> %s", 
                                       __FUNCTION__, __LINE__, GetLastError(), error);
      return false;
     }
//---
   return true;
  }

En consecuencia, en el cuerpo del método, comprobamos los punteros recibidos a los búferes de datos e indicamos los valores de los parámetros en la dimensión del espacio de tareas. El algoritmo de llamada al núcleo no ha cambiado.

La situación con el método de avance MHCA es similar. En los parámetros del método recibimos punteros a búferes de datos. El método CNeuronMH2AttentionOCL::attentionOut no tenía parámetros y utilizaba objetos internos en su funcionalidad. 

bool CNeuronMFTOCL::MHCA(CBufferFloat *q, CBufferFloat *kv, 
                         CBufferFloat *score, CBufferFloat *out)
  {
   if(!q || !kv || !score || !out)
      return false;

En el cuerpo del método, creamos las dimensiones del espacio de tareas.

   uint global_work_offset[3] = {0};
   uint global_work_size[3] = {iUnits, iWindow, iHeads};
   uint local_work_size[3] = {1, iWindow, 1};

Pasamos los parámetros al núcleo.

   ResetLastError();
   if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_q, q.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", 
                                             __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_kv, kv.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", 
                                             __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_score, score.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", 
                                             __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_out, out.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", 
                                             __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_MH2AttentionOut, def_k_mh2ao_dimension, (int)iWindowKey))
     {
      printf("Error of set parameter kernel %s: %d; line %d", 
                                             __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }

Añádelo a la cola.

   if(!OpenCL.Execute(def_k_MH2AttentionOut, 3, global_work_offset, global_work_size, 
                                                                    local_work_size))
     {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      return false;
     }
//---
   return true;
  }

El algoritmo de avance del MFT está implementado en el método CNeuronMFTOCL::feedForward. Al igual que los métodos similares de otras capas neuronales, el método recibe en sus parámetros un puntero a la capa neuronal anterior. En el cuerpo del método, comprobamos inmediatamente la relevancia del puntero recibido.

bool CNeuronMFTOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(CheckPointer(NeuronOCL) == POINTER_INVALID)
      return false;

A continuación, organizamos un bucle de cálculo secuencial de varios modos de interacción entre agentes.

  for(uint i = 0; (i < iLayers && !IsStopped()); i++)
     {
      //--- MHSA
      //--- Calculate Queries, Keys, Values
      CBufferFloat *inputs = NeuronOCL.getOutput();
      CBufferFloat *qkv = QKV_Tensors.At(i * 6);
      if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? 6 : 9)), 
                                           inputs, qkv, iWindow, 3 * iWindowKey * iHeads, None))
         return false;
 

Aquí obtenemos primero las entidades Query, Key y Value del bloque MHSA. A continuación definimos la matriz de coeficientes de atención.

      //--- Score calculation
      CBufferFloat *temp = S_Tensors.At(i * 4);
      if(IsStopped() || !AttentionScore(qkv, temp, false))
         return false;

Generar el resultado de la autoatención multicéfala.

      //--- Multi-heads attention calculation
      CBufferFloat *out = AO_Tensors.At(i * 4);
      if(IsStopped() || !AttentionOut(qkv, temp, out))
         return false;

Reducimos el resultado de la atención multicabezal al tamaño de los datos originales. Permítanme recordarles que los cabezales de atención se combinan en el marco de una variante de interacción entre agentes.

      //--- Attention out calculation
      temp = FF_Tensors.At(i * 8);
      if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 8 : 12)),

                                               out, temp, iWindowKey * iHeads, iWindow, None))
         return false;

Sumamos el tensor resultante con los datos originales y normalizamos el resultado de la operación.

      //--- Sum and normalize attention
      if(IsStopped() || !SumAndNormilize(temp, inputs, temp, iWindow))
         return false;

A continuación viene el bloque de atención cruzada (Cross-attention). Aquí definimos primero la entidad Query.

      //--- MHCA
      inputs = temp;
      CBufferFloat *q = QKV_Tensors.At(i * 6 + 1);
      if(IsStopped() || 
         !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? 6 : 9) + 1),
                             inputs, q, iWindow, iWindowKey * iHeads, None))
         return false;

A continuación, transponemos los datos originales y calculamos las entidades Key y Value.

      CBufferFloat *tr = cTranspose.At(i * 2);
      if(IsStopped() || !Transpose(inputs, tr, iUnits, iWindow))
         return false;
      CBufferFloat *kv = QKV_Tensors.At(i * 6 + 2);
      if(IsStopped() || 
         !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? 6 : 9) + 2), 
                             tr, kv, iUnits, 2 * iWindowKey * iHeads, None))
         return false;

Determinar los resultados de la atención cruzada multicabezal.

      //--- Multi-heads cross attention calculation
      temp = S_Tensors.At(i * 4 + 1);
      out = AO_Tensors.At(i * 4 + 1);
      if(IsStopped() || !MHCA(q, kv, temp, out))
         return false;

Los resultados obtenidos se comprimen al tamaño de los datos originales.

      //--- Cross Attention out calculation
      temp = FF_Tensors.At(i * 8 + 1);
      if(IsStopped() ||
         !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 1), 
                             out, temp, iWindowKey * iHeads, iWindow, None))
         return false;

Añádelo a los resultados del bloque MHSA y normaliza los datos.

      //--- Sum and normalize attention
      if(IsStopped() || !SumAndNormilize(temp, inputs, temp, iWindow))
         return false;

A continuación, organizamos la propagación de datos a través del bloque FeedForward.

      //--- Feed Forward
      inputs = temp;
      temp = FF_Tensors.At(i * 8 + 2);
      if(IsStopped() || 
         !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 2),
                             inputs, temp, iWindow, 4 * iWindow, LReLU))
         return false;
      out = FF_Tensors.At(i * 8 + 3);
      if(IsStopped() || 
         !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 3),
                                           temp, out, 4 * iWindow, iWindow, activation))
         return false;

Los resultados de la operación de bloque FeedForward se añaden a los resultados de MHCA. Los datos recibidos se normalizan y se escriben en el búfer de resultados con un desplazamiento correspondiente al modo de interacción analizado.

      //--- Sum and normalize out
      if(IsStopped() ||
         !SumAndNormilize(out, inputs, Output, iWindow, true, 0, 0, i * inputs.Total()))
         return false;
     }
//---
   return true;
  }

A continuación, pasamos al siguiente bucle de iteración para analizar el siguiente modo de interacción entre agentes en la escena.

Tras analizar con éxito todos los modos de interacción entre agentes, completamos el método con el resultado true.

Durante el paso feed-forward, realizamos la funcionalidad básica de analizar los modos de interacción de los agentes en la escena y predecir posibles variantes para eventos posteriores. Por lo tanto, el entrenamiento del modelo no es posible sin aplicar métodos de retropropagación. Durante el paso de retropropagación se optimizan los parámetros del modelo para obtener mejores resultados. Por lo tanto, después de implementar los métodos feed-forward, pasamos a construir el algoritmo de retropropagación.

Al igual que con el paso feed-forward, para implementar el paso de retropropagación necesitaremos crear un método adicional MHCAInsideGradients. Su algoritmo es casi completamente similar al algoritmo de CNeuronMH2AttentionOCL::AttentionInsideGradients. La única diferencia es que MHCAInsideGradients no trabaja con objetos internos de la clase, sino con buffers recibidos en parámetros de métodos. Ya hemos visto ejemplos de este tipo de modificaciones anteriormente en este artículo. Por lo tanto, no nos detendremos ahora en un examen detallado del método. Puede encontrar el código completo en el archivo adjunto.

El algoritmo de paso de retropropagación se implementa en el método calcInputGradients. En los parámetros, el método recibe un puntero al objeto de la capa neuronal anterior.

bool CNeuronMFTOCL::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(CheckPointer(prevLayer) == POINTER_INVALID)
      return false;

En el cuerpo del método, comprobamos inmediatamente la validez del puntero recibido. Después guardamos punteros a buffers en variables locales, con los que funcionarán todos los bloques de interacción paralela.

   CBufferFloat *out_grad = Gradient;
   CBufferFloat *inp = prevLayer.getOutput();
   CBufferFloat *grad = prevLayer.getGradient();

Una vez finalizado el trabajo preparatorio, organizamos un bucle para propagar los gradientes de error a través de bloques separados de interacción paralela.

   for(int i = 0; (i < (int)iLayers && !IsStopped()); i++)
     {
      //--- Passing gradient through feed forward layers
      if(IsStopped() ||
         !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 3), 
                                   Gradient, FF_Tensors.At(i * 8 + 2), FF_Tensors.At(i * 8 + 6),
                                    4 * iWindow, iWindow, None, i * inp.Total()))
         return false;

Aquí propagamos primero el gradiente de error a través del bloque FeedForward.

      CBufferFloat *temp = FF_Tensors.At(i * 8 + 5);
      if(IsStopped() ||
         !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 2),
                                    FF_Tensors.At(i * 8 + 6), FF_Tensors.At(i * 8 + 1), 
                                    temp, iWindow, 4 * iWindow, LReLU))
         return false;

En el paso feed-forward, los resultados de la operación del bloque FeedForward se añadieron a la salida del bloque MHCA. Del mismo modo, propagamos el gradiente de error en la dirección opuesta. Sin embargo, esta vez no realizamos la normalización.

      //--- Sum gradient
      if(IsStopped() || 
         !SumAndNormilize(Gradient, temp, temp, iWindow, false, i * inp.Total(), 0, 0))
         return false;
      out_grad = temp;

A continuación, tenemos que propagar el gradiente de error a través del módulo de atención cruzada. Aquí, primero propagamos el gradiente de error a través de las cabezas de atención.

      //--- MHCA
      //--- Split gradient to multi-heads
      if(IsStopped() || 
         !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 1),
                                                  out_grad, AO_Tensors.At(i * 4 + 1),
                                                  AO_Tensors.At(i * 4 + 3),
                                                  iWindowKey * iHeads, iWindow, None))
         return false;

A continuación, propagamos el gradiente de error a las entidades correspondientes.

      if(IsStopped() || 
         !MHCAInsideGradients(QKV_Tensors.At(i * 6 + 1), QKV_Tensors.At(i * 6 + 4), 
                              QKV_Tensors.At(i * 6 + 2), QKV_Tensors.At(i * 6 + 5),
                              S_Tensors.At(i * 4 + 1), AO_Tensors.At(i * 4 + 3)))
         return false;

Ahora necesitamos transponer el gradiente de error de Key y Value del bloque MHCA.

      CBufferFloat *tr = cTranspose.At(i * 2 + 1);
      if(IsStopped() || !Transpose(QKV_Tensors.At(i * 6 + 5), tr, iWindow, iUnits))
         return false;

En primer lugar, resumimos los resultados obtenidos.

      //--- Sum
      temp = FF_Tensors.At(i * 8 + 4);
      if(IsStopped() || !SumAndNormilize(QKV_Tensors.At(i * 6 + 4), tr, temp, iWindow, false))
         return false;

Luego les añadimos el gradiente de error a la salida del bloque MHCA.

      if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false))
         return false;

Después tenemos que propagar el gradiente de error a través del bloque MHSA. Al igual que con la atención cruzada, primero distribuimos el gradiente de error entre los cabezales de atención.

      //--- MHSA
      //--- Split gradient to multi-heads
      out_grad = temp;
      if(IsStopped() || 
         !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12)), 
                                    out_grad, AO_Tensors.At(i * 4), AO_Tensors.At(i * 4 + 2), 
                                    iWindowKey * iHeads, iWindow, None))
         return false;

Propagar el gradiente de error a las entidades correspondientes.

      //--- Passing gradient to query, key and value
      if(IsStopped() || 
         !AttentionInsideGradients(QKV_Tensors.At(i * 6), QKV_Tensors.At(i * 6 + 3), 
                                   S_Tensors.At(i * 4), S_Tensors.At(i * 4 + 1), 
                                   AO_Tensors.At(i * 4 + 1)))
         return false;

Propagar hasta el nivel de datos de origen.

      if(IsStopped() || 
         !ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? 6 : 9)), 
                                    QKV_Tensors.At(i * 6 + 3), inp, tr, iWindow, 
                                    3 * iWindowKey * iHeads, None))
         return false;

Ahora necesitamos sumar el gradiente de error en la entrada y salida del bloque MHSA y escribir el resultado en el buffer de gradiente de la capa anterior. Pero hay un punto. Necesitamos escribir la suma de los gradientes de error de todos los bloques de interacción paralela en el buffer de gradiente de la capa anterior. Por lo tanto, primero comprobamos el identificador del bloque de interacción paralelo. Para el primer bloque, simplemente escribimos la suma de los gradientes de 2 hilos en el buffer de gradiente de los errores de la capa anterior. Para los bloques restantes de interacción paralela, añadimos los gradientes de error resultantes a los datos del búfer de la capa anterior.

      //--- Sum gradients
      if(i > 0)
        {
         if(IsStopped() || !SumAndNormilize(grad, tr, grad, iWindow, false))
            return false;
         if(IsStopped() || !SumAndNormilize(out_grad, grad, grad, iWindow, false))
            return false;
        }
      else
         if(IsStopped() || !SumAndNormilize(out_grad, tr, grad, iWindow, false))
            return false;
     }
//---
   return true;
  }

Entonces pasamos a la siguiente iteración del bucle para pasar el gradiente de error a través del siguiente bloque de interacción paralela.

Después de completar con éxito todas las iteraciones del bucle, terminamos el método con 'true'.

Hemos organizado el paso feed-forward y la retropropagación del gradiente de error en la clase CNeuronMFTOCL. Para completar la implementación de la funcionalidad principal de la clase, necesitamos añadir un método para actualizar los parámetros de pesaje "updateInputWeights". El algoritmo del método es bastante sencillo. En un bucle, llamamos al método de la clase base ConvolutionUpdateWeights, en el que se actualizan los parámetros de un buffer independiente. Los búferes de datos necesarios para el funcionamiento se pasan en los parámetros del método.

bool CNeuronMFTOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(CheckPointer(NeuronOCL) == POINTER_INVALID)
      return false;
   CBufferFloat *inputs = NeuronOCL.getOutput();
   for(uint l = 0; l < iLayers; l++)
     {
      if(IsStopped() || 
         !ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? 6 : 9)), 
                                    QKV_Tensors.At(l * 6 + 3), inputs, 
                 (optimization == SGD ? QKV_Weights.At(l * 6 + 3) : QKV_Weights.At(l * 9 + 3)),
                 (optimization == SGD ? NULL : QKV_Weights.At(l * 9 + 6)), iWindow, 
                                    3 * iWindowKey * iHeads))
         return false;
      if(IsStopped() || 
         !ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? 6 : 9) + 1), 
                                    QKV_Tensors.At(l * 6 + 4), inputs, 
                 (optimization == SGD ? QKV_Weights.At(l * 6 + 4) : QKV_Weights.At(l * 9 + 4)), 
                 (optimization == SGD ? NULL : QKV_Weights.At(l * 9 + 7)), iWindow, 
                                    iWindowKey * iHeads))
         return false;
      if(IsStopped() || 
         !ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? 6 : 9) + 2), 
                                    QKV_Tensors.At(l * 6 + 5), cTranspose.At(l * 2), 
                 (optimization == SGD ? QKV_Weights.At(l * 6 + 5) : QKV_Weights.At(l * 9 + 5)), 
                 (optimization == SGD ? NULL : QKV_Weights.At(l * 9 + 8)), iUnits, 
                                    2 * iWindowKey * iHeads))
         return false;
      //---
      if(IsStopped() || 
         !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 8 : 12)), 
                                    FF_Tensors.At(l * 8 + 4), AO_Tensors.At(l * 4), 
                 (optimization == SGD ? FF_Weights.At(l * 8 + 4) : FF_Weights.At(l * 12 + 4)), 
                 (optimization == SGD ? NULL : FF_Weights.At(l * 12 + 8)), 
                                    iWindowKey * iHeads, iWindow))
         return false;
      if(IsStopped() || 
         !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 8 : 12) + 1), 
                                    FF_Tensors.At(l * 8 + 5), AO_Tensors.At(l * 4 + 1), 
                 (optimization == SGD ? FF_Weights.At(l * 8 + 5) : FF_Weights.At(l * 12 + 5)),
                 (optimization == SGD ? NULL : FF_Weights.At(l * 12 + 9)), 
                                    iWindowKey * iHeads, iWindow))
         return false;
      //---
      if(IsStopped() || 
         !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 8 : 12) + 2), 
                                    FF_Tensors.At(l * 8 + 6), FF_Tensors.At(l * 8 + 1), 
                 (optimization == SGD ? FF_Weights.At(l * 8 + 6) : FF_Weights.At(l * 12 + 6)),
                 (optimization == SGD ? NULL : FF_Weights.At(l * 12 + 10)), 
                                    iWindow, 4 * iWindow))
         return false;
      //---
      if(IsStopped() || 
         !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 8 : 12) + 3),
                                    FF_Tensors.At(l * 8 + 7), FF_Tensors.At(l * 8 + 2), 
                 (optimization == SGD ? FF_Weights.At(l * 8 + 7) : FF_Weights.At(l * 12 + 7)), 
                 (optimization == SGD ? NULL : FF_Weights.At(l * 12 + 11)), 
                                    4 * iWindow, iWindow))
         return false;
     }
//---
   return true;
  }

Esto concluye la implementación de la funcionalidad principal del algoritmo MFT (Multi-future Transformer) en la clase CNeuronMFTOCL. Puedes familiarizarte con la implementación de los métodos auxiliares utilizando los códigos del archivo adjunto. Allí también encontrará el código completo de todos los programas utilizados al escribir el artículo. Ahora pasamos a la aplicación de Asesores Expertos para construir y entrenar los modelos.

2.2 Arquitectura modelo

Hemos construido una clase para un algoritmo de bloques de interacción paralela, que fue propuesto por los autores del método Multi-future Transformer. Sin embargo, esto es sólo una capa de nuestro modelo. Ahora tenemos que describir la arquitectura completa de los modelos creados. Los modelos se alimentarán de datos fuente sin procesar. Como resultado del trabajo de los modelos, necesitamos obtener un vector de acciones óptimas del Agente, cuya aplicación puede reportar beneficios cuando se trabaja en los mercados financieros.

Debo decir que al construir mi modelo, me desvié un poco de la arquitectura propuesta por los autores del método. Hay varias razones para ello.

En primer lugar, se propuso el método Multi-future Transformer para predecir la interacción de los vehículos autónomos con el entorno. Tenemos previsto utilizar nuestro modelo para los mercados financieros. Ambas tareas tienen sus propias especificidades, que dejan huella en la construcción de los modelos.

En segundo lugar, en el anterior artículo, prestamos mucha atención a la optimización del rendimiento del modelo. Me gustaría aprovechar la experiencia adquirida. Por lo tanto, he utilizado el modelo de arquitectura del artículo anterior como donante. Se han introducido cambios en los modelos de planificación de trayectorias.

La arquitectura de los modelos de predicción de trayectorias futuras se describe en el método CreateTrajNetDescriptions del archivo "...\Experts\MFT\Trajectory.mqh". En los parámetros, el método recibe punteros a matrices dinámicas para crear 3 modelos:

  • Estado del codificador
  • Descodificador de extremos
  • Probabilidades de escena

En artículos anteriores, decidimos no predecir la trayectoria completa del movimiento de los precios, sino que nos concentramos en predecir los principales extremos del movimiento de los precios:

  • Close
  • High
  • Low

Por lo tanto, en nuestro trabajo consideramos el estado de la dinámica del movimiento de los precios como variantes de la escena. No analizamos las probabilidades de las trayectorias finales de los agentes individuales.

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

En el cuerpo del método, comprobamos la pertinencia de los punteros a objetos recibidos en los parámetros y, si es necesario, creamos otros nuevos.

En primer lugar, describimos la arquitectura del codificador, al que alimentamos con datos iniciales brutos que describen el estado actual del entorno. 

//--- 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;
     }

Como es habitual, los datos recibidos se preprocesan en la capa de normalización por lotes.

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

Después creamos una incrustación del estado actual y la guardamos en la pila interna del modelo.

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

Cada elemento incrustado se asocia a un agente distinto de la escena ambiental. Según el método MFT, implementaremos la codificación posicional de los datos de origen.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronPEOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Más adelante en la arquitectura del método MFT original viene una pila de bloques MHSA. He aquí mi primera desviación de la arquitectura propuesta. Decidí utilizar el trabajo del artículo anterior y dejé 2 capas Crystal-GCN separadas por una capa de normalización de lotes.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCGConvOCL;
   descr.count = prev_count * prev_wout;
   descr.window = descr.count;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_wout;
   descr.batch = MathMax(1000, GPTBars);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCGConvOCL;
   descr.count = prev_count * prev_wout;
   descr.window = descr.count;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Le sigue una capa de MHSA.

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

A continuación, los autores del método proponen promediar la dinámica de cada agente. Aquí, en el bloque de interacción paralela, realizan análisis cruzados de datos con datos de mapas contextuales. En nuestro caso, no existe un mapa de posibles movimientos de precios próximos. En su lugar, utilizaremos el análisis contextual de las trayectorias de los rasgos analizados.

Lo siguiente en nuestra arquitectura es el bloque de computación paralela creado anteriormente. En este bloque, utilizamos los bloques Self-Attention y Cross-Attention con 4 cabezales de atención. Los autores del método utilizaron en su trabajo sólo 1 cabezal de atención en bloques de interacción paralelos. El número de modos de interacción de los agentes viene determinado por la constante NForecast.

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

Preste atención al siguiente punto arquitectónico. En nuestra implementación, construimos la clase CNeuronMFTOCL de tal forma que sus resultados pueden representarse como un tensor tridimensional {número de elementos de secuencia, modo de interacción, número de agentes}. Este formato de datos no es muy cómodo para el procesamiento posterior, ya que entonces tenemos que trabajar con cada modo de interacción individual. La ubicación del modo de interacción dentro de las dimensiones del tensor complica este trabajo. Por lo tanto, transponemos los datos para trasladar el modo de interacción del agente a la primera dimensión del tensor.

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   descr.window = prev_wout * NForecast;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Así, a la salida del codificador recibimos el tensor {modo de interacción del agente, número de agentes, longitud del vector que describe la dinámica del agente}.

Después del codificador, creamos el modelo de decodificador de punto final. Introducimos en el modelo el tensor de los resultados del codificador.

//--- Endpoints
   endpoints.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = (prev_count * prev_wout) * NForecast;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!endpoints.Add(descr))
     {
      delete descr;
      return false;
     }

Permítanme recordarles que el método MTF requiere el uso de un descodificador con los mismos parámetros para todos los modos de interacción de los agentes. Podemos implementar un enfoque similar utilizando una capa convolucional con un tamaño de ventana y un paso igual al tensor de un modo de interacción del agente. Dividiremos esta etapa en 2 fases. Primero colapsamos la dinámica de cada agente individual.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = NForecast * prev_wout;
   descr.window = prev_count;
   descr.step = descr.window;
   descr.window_out = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!endpoints.Add(descr))
     {
      delete descr;
      return false;
     }

Y luego tenemos opciones de escena para cada modo de interacción analizado.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = NForecast;
   descr.window = LatentCount * prev_wout;
   descr.step = descr.window;
   descr.window_out = 3;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!endpoints.Add(descr))
     {
      delete descr;
      return false;
     }

El modelo para evaluar las probabilidades de las distintas escenas previstas se mantuvo prácticamente sin cambios. Como en el caso del descodificador, los resultados del codificador se introducen en el modelo.

//--- Probability
   probability.Clear();
//--- Input layer
   if(!probability.Add(endpoints.At(0)))
      return false;

Se combinan con opciones de escenas predictivas.

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

Los datos se analizan mediante 2 capas totalmente conectadas.

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

Y se normaliza mediante la función SoftMax.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = NForecast;
   descr.step = 1;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

La arquitectura de los modelos Actor es prácticamente la misma. Sólo tuve que hacer pequeñas modificaciones en cuanto al procesamiento de los resultados del codificador de estado actual.

También hay que señalar que los cambios introducidos en la arquitectura del modelo no tuvieron prácticamente ninguna repercusión en los algoritmos de interacción con el entorno y de entrenamiento del modelo. Por tanto, todos los EAs han sido copiados del anterior artículos con mínimas ediciones. Por lo tanto, no me detendré en la descripción de sus algoritmos en el marco de este artículo. De todas formas, puedes encontrar el código completo de todos los programas utilizados en el artículo en el archivo adjunto.

3. Pruebas

Hemos implementado el algoritmo Multi-future Transformer utilizando herramientas MQL5 y descrito la arquitectura de modelos que nos permiten utilizar los enfoques propuestos para entrenar modelos de predicción de varias opciones para el próximo movimiento de precios con una evaluación de la probabilidad de su implementación. Ahora es el momento de comprobar los resultados de nuestro trabajo con datos reales en el probador de estrategias de MetaTrader 5.

Como siempre, el modelo se entrena y se prueba utilizando datos históricos del EURUSD H1. Los datos de los 7 primeros meses de 2023 se utilizan para entrenar los modelos. Para probar el modelo entrenado, utilizamos datos históricos de agosto de 2023.

Para entrenar el modelo, utilicé el conjunto de datos de entrenamiento y el EA de entrenamiento del artículo anterior. Por lo tanto, puede observarse que los cambios en los resultados obtenidos se deben principalmente a un cambio en la arquitectura del modelo. Por supuesto, no podemos excluir el factor de aleatoriedad introducido por la inicialización del modelo con parámetros aleatorios. Además, el muestreo de datos del búfer de repetición de experiencias durante el proceso de entrenamiento también es aleatorio. Pero la influencia de este factor se minimiza al aumentar las épocas de entrenamiento.

En el artículo anterior, el modelo mostraba un resultado bastante estable, pero tiene un número muy reducido de operaciones. En el nuevo modelo, se observa un aumento del número de operaciones al tiempo que se mantiene un resultado positivo.

Durante el mes de agosto de 2023, el modelo ejecutó 13 operaciones, 6 de las cuales se cerraron con beneficio. El modelo arrojó un factor de beneficio de 1,63 durante el periodo de prueba. 


Conclusión

En este artículo, nos familiarizamos con otro método para predecir el próximo movimiento de precios - Multi-future Transformer. Una de las características clave de este método es la construcción de previsiones multimodales para el movimiento de los agentes, haciendo hincapié en su interacción entre sí y con la escena ambiental. Esto nos permite hacer previsiones más precisas de los próximos movimientos.

En la parte práctica, implementamos los enfoques propuestos utilizando MQL5. Entrenamos y probamos el modelo utilizando datos reales en el probador de estrategias de MetaTrader 5. Los resultados obtenidos confirman la eficacia de los planteamientos propuestos. Podemos observar la diversidad de las previsiones obtenidas, que se consigue gracias al aislamiento de las previsiones de las modalidades individuales manteniendo el análisis de la interacción de los agentes.


Referencias

  • Multi-future Transformer: Learning diverse interaction modes for behaviour prediction in autonomous driving
  • Otros artículos de esta serie

  • Programas utilizados en el artículo

    # Nombre Tipo Descripción
    1 Research.mq5 Asesor experto Colección de ejemplos
    2 ResearchRealORL.mq5
    Asesor experto
    EA para la recolección de ejemplos utilizando el método Real-ORL
    3 Study.mq5  Asesor experto EA para el entrenamiento del modelo
    4 Test.mq5 Asesor experto EA para la prueba del modelo
    5 Trajectory.mqh Biblioteca de clases Estructura de descripción del estado del sistema
    6 NeuroNet.mqh Biblioteca de clases Una biblioteca de clases para crear una red neuronal
    7 NeuroNet.cl Código base Biblioteca de código de programa OpenCL


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

    Archivos adjuntos |
    MQL5.zip (868.32 KB)
    Redes neuronales: así de sencillo (Parte 77): Transformador de covarianza cruzada (XCiT) Redes neuronales: así de sencillo (Parte 77): Transformador de covarianza cruzada (XCiT)
    En nuestros modelos, a menudo utilizamos varios algoritmos de atención. Y, probablemente, lo más frecuente es utilizar transformadores. Su principal desventaja es la necesidad de recursos. En este artículo, estudiaremos un nuevo algoritmo que puede ayudar a reducir los costes informáticos sin perder calidad.
    DoEasy. Elementos de control (Parte 33): "ScrollBar" vertical DoEasy. Elementos de control (Parte 33): "ScrollBar" vertical
    En este artículo, continuaremos desarrollando los elementos gráficos de la librería DoEasy, y añadiremos el desplazamiento vertical de los controles del objeto formulario y algunas funciones y métodos útiles que serán necesarios más adelante.
    Características del Wizard MQL5 que debe conocer (Parte 12): Polinomio de Newton Características del Wizard MQL5 que debe conocer (Parte 12): Polinomio de Newton
    El polinomio de Newton, que crea ecuaciones cuadráticas a partir de un conjunto de unos pocos puntos, es un enfoque arcaico pero interesante para observar una serie temporal. En este artículo tratamos de explorar qué aspectos podrían ser de utilidad para los operadores desde este enfoque, así como abordar sus limitaciones.
    Desarrollo y prueba de sistemas comerciales basados en el canal de Keltner Desarrollo y prueba de sistemas comerciales basados en el canal de Keltner
    En este artículo examinaremos los sistemas comerciales que utilizan un concepto muy importante de los mercados financieros: la volatilidad. Asimismo, estudiaremos un sistema comercial basado en el Canal de Keltner, incluyendo su implementación en código y sus pruebas con varios activos.