Русский Português
preview
Redes neuronales en el trading: descomposición en lugar de escalado — Construcción de módulos

Redes neuronales en el trading: descomposición en lugar de escalado — Construcción de módulos

MetaTrader 5Sistemas comerciales |
136 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

En el artículo anterior, empezamos a implementar nuestra propia visión de los enfoques propuestos por los autores del framework SSCNN. En este artículo continuaremos: pasaremos a la parte práctica de la implementación de los componentes clave del framework. Sin embargo, antes de empezar a trabajar con el código y los algoritmos, es importante recordar al lector en qué consisten los puntos fuertes de SSCNN y cuáles son los principios de su funcionamiento. Comprender estos fundamentos permitirá no solo reproducir la implementación, sino también adaptarla de forma consciente a tareas reales de trading.

El framework SSCNN (Segmental Structured Convolutional Neural Network) se creó como una herramienta especializada para el análisis de series temporales en condiciones de inestabilidad del mercado y fragmentación de las señales. Su idea central consiste en procesar no valores aislados, sino segmentos de datos íntegros, lo que permite al modelo tener en cuenta tanto las características locales como las dependencias contextuales más amplias. En lugar de tratar la serie temporal de forma lineal, SSCNN propone un enfoque estratificado: cada intervalo temporal se analiza dentro de la estructura del contexto que lo rodea, lo que permite extraer con precisión características relevantes incluso con un alto nivel de ruido.

En la primera etapa, los datos pasan por un proceso de descomposición estructural, en el que la señal se divide en componentes estables: la tendencia a largo plazo, las oscilaciones estacionales, las fluctuaciones a corto plazo y el ruido residual. Este paso es especialmente importante cuando se trabaja con series financieras, en las que la tendencia y las desviaciones a corto plazo tienen un peso analítico diferente. Para cada componente se crean modelos de procesamiento independientes, lo que permite mejorar la calidad del análisis y la interpretabilidad de los resultados. El pronóstico final se construye como una combinación coherente de las salidas de estos modelos.

En SSCNN se presta especial atención al mecanismo de normalización basado en atención (AttnNorm). Este componente no solo estabiliza las representaciones internas del modelo, sino que también le permite centrarse de forma adaptativa en las partes de los datos que aportan más información. En condiciones de turbulencia del mercado, donde el valor de la señal puede verse muy distorsionado por picos a corto plazo, esta selectividad aporta robustez y precisión al pronóstico. La normalización se implementa teniendo en cuenta el contexto segmental e incluye el entrenamiento de los parámetros de atención, lo que hace que la retropropagación del error sea más informativa y dirigida.

A continuación se presenta la visualización del framework SSCNN elaborada por el autor.


Módulo de extracción de componentes

En la parte práctica del artículo anterior nos centramos en la implementación del kernel de normalización con coeficientes de atención en el entorno OpenCL. Este trabajo nos permitió sentar las bases para la ejecución rápida y paralela de operaciones clave: el cálculo de medias y desviaciones estándar por segmentos de datos, teniendo en cuenta el peso de atención, así como la normalización directa de la señal original. Todo ello se ejecuta en el contexto de la GPU, lo que garantiza un alto rendimiento y la escalabilidad de la solución.

Hoy continuamos, como es lógico, el trabajo iniciado anteriormente y trasladamos el foco a la interacción con estos kernels de OpenCL desde el programa principal. Tenemos que implementar los algoritmos para su gestión, lo que incluye la preparación de los búferes, la configuración de los parámetros de llamada, la sincronización y el control de la integridad de los datos transmitidos. Se trata de una etapa importante, ya que la eficiencia del framework SSCNN depende en gran medida no solo de la precisión matemática del kernel, sino también de lo bien organizado y coordinado que esté el intercambio de datos entre la CPU y el acelerador gráfico.

El elemento central del diseño es el objeto CNeuronAttentNorm. Es precisamente este objeto el que se encarga de coordinar la interacción entre la lógica de alto nivel de la capa de normalización con atención y el cómputo de bajo nivel en la GPU. Este objeto hereda de la clase base CNeuronBaseOCL, lo que le proporciona una interfaz universal y le permite integrarse en la pila general del modelo sin perder flexibilidad. A continuación se muestra la estructura del nuevo objeto.

class CNeuronAttentNorm :  public CNeuronBaseOCL
  {
protected:
   uint                    iPeriod;
   uint                    iVariables;
   uint                    iCount;
   //---
   CParams                 cAttention;
   CNeuronSoftMaxOCL       cSoftMax;
   CNeuronBaseOCL          cMeans;
   CNeuronBaseOCL          cSTDevs;
   //---
   virtual bool      AttentNorm(CNeuronBaseOCL *NeuronOCL);
   virtual bool      AttentNormGrad(CNeuronBaseOCL *NeuronOCL);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronAttentNorm(void) :  iPeriod(0), iVariables(0), iCount(0) {};
                    ~CNeuronAttentNorm(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint period, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronAttentNorm; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   CNeuronBaseOCL*   GetMeans(void) { return cMeans.AsObject(); }
   CNeuronBaseOCL*   GetSTDevs(void) { return cSTDevs.AsObject(); }
   virtual uint      GetPeriod(void) const { return iPeriod; }
   virtual uint      GetVariables(void) const { return iVariables; }
   virtual uint      GetUnits(void) const { return iCount; }
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override {  activation = None; }
  };

En la estructura de la clase vemos que los parámetros internos clave son iPeriod, iVariables e iCount: estos definen, respectivamente, el tamaño del segmento de datos, el número de variables y la cantidad de segmentos en una secuencia unitaria. Estos valores determinan la estructura del array analizado.

Conviene prestar especial atención al objeto cAttention, que almacena los parámetros relacionados con el mecanismo de atención, y a cSoftMax, el componente que implementa la transformación de los pesos de atención en una forma normalizada. Estos dos módulos funcionan en estrecha colaboración: los parámetros entrenables de atención pasan por una transformación SoftMax y, solo después, se utilizan para calcular las medias y las desviaciones estándar. Para almacenar estas estadísticas se utilizan los objetos cMeans y cSTDevs.

Todos los objetos internos de la clase CNeuronAttentNorm están declarados como miembros embebidos del objeto, lo que garantiza la previsibilidad de su ciclo de vida. Este enfoque permite dejar vacíos tanto el constructor como el destructor. La asignación y liberación de recursos quedan delegadas en una gestión explícita dentro del ciclo de vida del modelo. La inicialización principal de todos los componentes internos y heredados se lleva a cabo de forma centralizada en el método Init, que recibe los parámetros del entorno de ejecución y de configuración de la capa.

bool CNeuronAttentNorm::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                             uint units_count, uint period, uint variables,
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_count * period * variables,
                                                                 optimization_type, batch))
      return false;
   CNeuronBaseOCL::SetActivationFunction(None);

En primer lugar, la ejecución pasa al método homónimo de la clase base, donde ya está implementada la inicialización de todas las interfaces heredadas. A continuación, forzamos el tipo de función de activación a None, ya que esta capa por sí sola no realiza transformaciones no lineales: toda su tarea se centra en la normalización teniendo en cuenta los pesos de atención.

Luego guardamos los parámetros de configuración de la capa: el número de bloques (unidades), el número de variables y el periodo de segmentación. Estos valores se utilizarán posteriormente para controlar la lógica interna de normalización.

   iCount = units_count;
   iVariables = variables;
   iPeriod = period;

Acto seguido, inicializamos los objetos recién declarados. El primero es el objeto cAttention, encargado de generar los pesos de atención. Tras inicializarlo correctamente, desactivamos la función de activación, dejando el resultado en forma lineal.

   if(!cAttention.Init(0, 0, OpenCL, iPeriod * iVariables, optimization, iBatch))
      return false;
   cAttention.SetActivationFunction(None);
   if(!cSoftMax.Init(0, 1, OpenCL, cAttention.Neurons(), optimization, iBatch))
      return false;
   cSoftMax.SetHeads(iVariables);

Luego se configura el componente cSoftMax, que transforma los valores de atención en una distribución probabilística. Su dimensionalidad corresponde al número de neuronas en cAttention. El método SetHeads determina el número de cabezas de atención; en este caso, se iguala al número de variables, lo que permite procesar cada característica de forma independiente.

A continuación, se crea el objeto cMeans, que se utilizará para almacenar los valores medios por segmento. La dimensionalidad viene determinada por el número de bloques multiplicado por el número de variables. La función de activación se desactiva de nuevo, ya que se trata de una característica estadística.

//---
   if(!cMeans.Init(0, 2, OpenCL, iCount * iVariables, optimization, iBatch))
      return false;
   cMeans.SetActivationFunction(None);
   if(!cSTDevs.Init(0, 3, OpenCL, iCount * iVariables, optimization, iBatch))
      return false;
   cSTDevs.SetActivationFunction(None);
//---
   return true;
  }

Del mismo modo se configura el bloque cSTDevs, responsable de la varianza (o, más precisamente, de la desviación estándar) necesaria para la normalización. Reproduce íntegramente la configuración de cMeans.

De este modo, el método Init constituye una base arquitectónica sólida para la capa CNeuronAttentNorm, al integrar los componentes lógicos y computacionales en un sistema unificado y fácilmente escalable. Cada objeto se encarga de una parte del procesamiento estrictamente definida, y la inicialización centralizada permite un control total sobre los parámetros de la capa y su comportamiento en el contexto de OpenCL.

Una vez finalizada la inicialización de todos los componentes internos, la etapa clave consiste en organizar la pasada directa a través de la capa. Es precisamente en esta etapa cuando el modelo comienza a utilizar las estructuras inicializadas anteriormente (pesos de atención, transformación SoftMax y objetos que almacenan estadísticas) para llevar a cabo la tarea principal: la normalización de los datos de entrada teniendo en cuenta la importancia de cada elemento. El algoritmo se ha implementado en el método feedForward.

bool CNeuronAttentNorm::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(bTrain)
     {
      if(!cAttention.FeedForward())
         return false;
      if(!cSoftMax.FeedForward(cAttention.AsObject()))
         return false;
     }
//---
   return AttentNorm(NeuronOCL);
  }

El método recibe como argumento un puntero a un objeto NeuronOCL, que representa la capa neuronal anterior que contiene los datos que se van a analizar.

Si el modelo se encuentra en modo de entrenamiento (bTrain == true), lo primero que se hace es realizar una pasada directa por el componente cAttention, en el que se generan los valores brutos de atención. Estos valores no dependen directamente del estado actual de los datos de entrada y se generan durante el proceso de entrenamiento del modelo. Por lo tanto, durante el funcionamiento, los resultados del bloque serán estáticos y no tendremos que realizar operaciones innecesarias en cada pasada.

A continuación, los coeficientes de atención generados se envían al bloque cSoftMax, que los transforma en una distribución de probabilidad normalizada. Este paso es fundamental: garantiza que la suma de los pesos de atención dentro de cada segmento sea igual a uno y, con ello, asegura la corrección de los cálculos posteriores.

Independientemente del modo actual (ya sea entrenamiento o inferencia), la acción final del método feedForward consiste en llamar a la función AttentNorm. Esta función actúa como envoltorio del kernel OpenCL del mismo nombre y se encarga de toda la preparación de bajo nivel: desde la configuración de los parámetros hasta el encolado de la tarea para su ejecución. La estructura del método sigue estrictamente el esquema estándar adoptado para todas las llamadas de este tipo, y esta estandarización resulta especialmente evidente gracias a las macros implementadas anteriormente y a las plantillas universales. Por ello, deliberadamente no analizaremos en detalle la implementación interna de AttentNorm, y nos limitaremos a señalar que se trata de un punto de integración típico y técnicamente correcto entre la lógica del modelo y el contexto de OpenCL.

Una vez finalizada la pasada directa, llega una etapa no menos importante: el cálculo de los gradientes de error necesarios para actualizar correctamente los pesos del modelo. Este proceso está implementado en el método calcInputGradients, y su ejecución sigue una lógica clara de retropropagación del error.

bool CNeuronAttentNorm::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!AttentNormGrad(NeuronOCL))
      return false;
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(),
                       NeuronOCL.getGradient(), NeuronOCL.Activation()))
         return false;

El método comienza con la llamada a AttentNormGrad, que, por analogía con AttentNorm, sirve de interfaz con el kernel OpenCL del mismo nombre. Su función consiste en recopilar los gradientes de la señal de salida normalizada, teniendo en cuenta las particularidades de la atención y los parámetros del segmento de datos actual. Este es un paso clave, ya que sobre esta base se contabiliza en sentido inverso la influencia de los datos de entrada en las estadísticas finales, como la media y la varianza.

A continuación, se realiza una comprobación: si se ha definido una función de activación para la capa de origen, es necesario diferenciar los gradientes obtenidos teniendo en cuenta la no linealidad especificada. Esto se implementa mediante la llamada a la función DeActivation, que corrige los gradientes y garantiza así la plena conformidad con el esquema clásico de la pasada inversa.

El paso final del método consiste en propagar los gradientes en sentido inverso a lo largo de la cadena de atención. Para ello se utiliza el método CalcHiddenGradients del objeto cAttention, al que se pasa como entrada el gradiente de salida del módulo SoftMax. Este procedimiento garantiza la continuidad del flujo de información de gradiente desde la capa de salida hacia los parámetros ocultos del modelo y, en última instancia, hacia sus entradas.

   if(!cAttention.CalcHiddenGradients(cSoftMax.AsObject()))
      return false;
//---
   return true;
  }

El proceso de actualización de los parámetros en el módulo de normalización con atención finaliza con el método updateInputWeights, que se encarga de ajustar los pesos internos a partir de los gradientes acumulados. En este caso, la implementación del método es extremadamente concisa: el control se delega en la función homónima del objeto cAttention, que se encarga de generar los coeficientes de atención. Esto se ajusta plenamente a la arquitectura general del framework SSCNN, en el que cada componente del modelo se encarga de su parte de los cálculos.

bool CNeuronAttentNorm::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   return cAttention.UpdateInputWeights();
  }

Este enfoque garantiza una alta modularidad y flexibilidad del sistema: el mecanismo de atención está aislado en su propio objeto, lo que facilita tanto la depuración como la posterior adaptación del modelo a nuevas condiciones. Además, la delegación directa del control hace que el código sea más limpio y fácil de leer, y favorece la reutilización de los componentes sin necesidad de reescribirlos.

Con esto concluimos el análisis de los métodos de la capa de normalización con coeficientes de atención. Hemos seguido toda la cadena lógica, desde la inicialización de los objetos internos hasta las etapas finales de la pasada directa y de la pasada inversa, incluyendo la distribución de los gradientes y la actualización de los pesos. Cabe destacar que todos los cálculos clave y las operaciones de control se han realizado dentro de una interfaz única y coherente.

El código fuente completo de la clase CNeuronAttentNorm y todos sus métodos se incluyen en el archivo adjunto.


Extracción de la componente espacial

Al pasar al siguiente bloque lógico de la arquitectura, examinemos un objeto destinado a la extracción de la componente espacial en el proceso de normalización con atención. Se trata de la clase CNeuronSAttentNorm, que amplía la funcionalidad de CNeuronTransposeOCL y, a diferencia de la implementación anterior, está orientada al procesamiento de datos transpuestos, lo que proporciona una mayor flexibilidad a la hora de trabajar con características espaciales y contextualmente distribuidas.

class CNeuronSAttentNorm :   public CNeuronTransposeOCL
  {
protected:
   CNeuronTransposeOCL     cTranspose;
   CNeuronBaseOCL          cAttention;
   CNeuronSoftMaxOCL       cSoftMax;
   CNeuronBaseOCL          cMeans;
   CNeuronBaseOCL          cSTDevs;
   CNeuronBaseOCL          cNorm;
   //---
   virtual bool      AttentNorm(CNeuronBaseOCL *NeuronOCL);
   virtual bool      AttentNormGrad(CNeuronBaseOCL *NeuronOCL);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSAttentNorm(void) {};
                    ~CNeuronSAttentNorm(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronSAttentNorm; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   CNeuronBaseOCL*   GetMeans(void) { return cMeans.AsObject(); }
   CNeuronBaseOCL*   GetSTDevs(void) { return cSTDevs.AsObject(); }
   virtual uint      GetVariables(void) const { return iWindow; }
   virtual uint      GetUnits(void) const { return iCount; }
   //---
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override {  activation = None; }
  };

La estructura del objeto es una especie de reflejo especular de la capa CNeuronAttentNorm analizada anteriormente, pero con una diferencia clave en la generación de los coeficientes de atención. Por su parte, la normalización propiamente dicha se realiza ya a lo largo del eje de características dentro de la ventana.

Al igual que antes, el proceso de inicialización del objeto CNeuronSAttentNorm se concentra en el método homónimo Init, que se encarga de configurar correctamente todos los componentes internos de la capa y de prepararlos para la ejecución de las tareas de la pasada directa e inversa.

bool CNeuronSAttentNorm::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                              uint units_count, uint variables,
                              ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronTransposeOCL::Init(numOutputs, myIndex, open_cl, units_count, variables,
                                                            optimization_type, batch))
      return false;
   activation = None;

En primer lugar, se llama al método homónimo de la clase base CNeuronTransposeOCL, que proporciona la configuración básica de los parámetros de la capa actual, incluidas las interfaces heredadas. A continuación, la función de activación de la capa se establece explícitamente en None, ya que en esta fase no se aplica la activación.

A continuación, comienza la inicialización secuencial de todos los objetos internos, cada uno de los cuales desempeña una función específica en el proceso de cálculo. El objeto cTranspose se encarga de transponer el tensor de entrada, convirtiendo el corte temporal en un vector de características con el que posteriormente trabajará el módulo de atención.

   if(!cTranspose.Init(0, 0, OpenCL, iWindow, iCount, optimization, iBatch))
      return false;

A continuación, se crea y configura el objeto cAttention, que en este caso actúa principalmente como búfer de almacenamiento de los coeficientes de atención.

   if(!cAttention.Init(0, 1, OpenCL, iWindow * iWindow, optimization, iBatch))
      return false;
   cAttention.SetActivationFunction(None);
   if(!cSoftMax.Init(0, 2, OpenCL, cAttention.Neurons(), optimization, iBatch))
      return false;
   cSoftMax.SetHeads(iWindow);

Luego viene el bloque asociado cSoftMax, que normaliza los pesos a lo largo de la dimensión especificada; en este caso, la dimensión de las características.

Acto seguido, se inicializan los objetos de medias cMeans y de desviaciones estándar cSTDevs, necesarios para el escalado y la alineación posteriores de los datos analizados.

   if(!cMeans.Init(0, 3, OpenCL, iCount, optimization, iBatch))
      return false;
   cMeans.SetActivationFunction(None);
   if(!cSTDevs.Init(0, 4, OpenCL, iCount, optimization, iBatch))
      return false;
   cSTDevs.SetActivationFunction(None);

La configuración concluye con el objeto cNorm, que es un búfer en el que se almacenan los datos normalizados antes de su transposición inversa a la representación de series temporales.

   if(!cNorm.Init(0, 5, OpenCL, Neurons(), optimization, iBatch))
      return false;
   cNorm.SetActivationFunction(None);
//---
   return true;
  }

Una vez finalizada la inicialización, el objeto CNeuronSAttentNorm pasa a ser plenamente funcional, por lo que podemos pasar a analizar la etapa clave: la pasada directa. Este método desempeña un papel central en el procesamiento de los datos analizados, transformándolos por etapas según los bloques de cálculo internos de la capa.

bool CNeuronSAttentNorm::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cTranspose.FeedForward(NeuronOCL))
      return false;

La pasada directa comienza con la llamada al método FeedForward del objeto cTranspose, que se encarga de la transformación inicial del tensor de entrada. En esta etapa, los datos se reorganizan de tal forma que la ventana temporal quede desplegada en una representación espacial. Esto es necesario para el trabajo posterior con la matriz de atención, en la que cada característica se comparará con todas las demás.

El siguiente paso es llamar a la función MatMul, que realiza la multiplicación matricial de los datos de entrada por su copia transpuesta. Aquí se crea una matriz de atención, en la que cada fila contiene una estimación de la importancia de los demás elementos de los datos analizados en relación con la característica actual. El resultado de esta operación se almacena en el objeto cAttention, que representa un mapa de atención en bruto, aún sin normalizar.

   if(!MatMul(NeuronOCL.getOutput(), cTranspose.getOutput(), cAttention.getOutput(),
              iWindow, iCount, iWindow, 1, false))
      return false;
   if(!cSoftMax.FeedForward(cAttention.AsObject()))
      return false;

Una vez obtenida la matriz de atención, es necesario normalizarla. Para ello, se invoca el método FeedForward del objeto cSoftMax, que convierte los valores numéricos en pesos probabilísticos, garantizando así la interpretabilidad y la estabilidad numérica de los cálculos posteriores.

Los pesos de atención obtenidos se pasan a continuación al método AttentNorm, que implementa la idea principal de la capa: el escalado y la normalización de los datos de entrada teniendo en cuenta los coeficientes de atención calculados. Esta etapa combina las propiedades estadísticas de la ventana (media y desviación estándar) teniendo en cuenta la importancia de cada característica, lo que permite al modelo identificar patrones estructurales en los datos incluso en presencia de ruido significativo o sesgos.

   if(!AttentNorm(cTranspose.AsObject()))
      return false;
//---
   return CNeuronTransposeOCL::feedForward(cNorm.AsObject());
  }

El paso final consiste en pasar el control al método feedForward de la clase base CNeuronTransposeOCL, donde se procesa el tensor de datos normalizados cNorm. De este modo, los resultados obtenidos se devuelven a la representación de los datos de entrada.

Una vez analizada la pasada directa, lo lógico es pasar a una etapa no menos importante: la retropropagación del error. El método calcInputGradients se encarga de calcular los gradientes necesarios para el posterior ajuste de los pesos y los parámetros del modelo durante el proceso de entrenamiento. Aquí se implementa una cadena de operaciones por etapas que garantiza la propagación de la señal de error desde la capa de salida del modelo hasta su entrada, teniendo en cuenta todas las transformaciones internas, incluidos el mecanismo de atención y la normalización.

bool CNeuronSAttentNorm::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//---
   if(!CNeuronTransposeOCL::calcInputGradients(cNorm.AsObject()))
      return false;

El procedimiento comienza con una comprobación básica de la validez del puntero al objeto de datos de entrada NeuronOCL, al que vamos a pasar el gradiente de error. A continuación, el control pasa al método calcInputGradients de la clase padre, pero esta vez con el objeto cNorm, que representa la salida normalizada del módulo de atención. Esto permite propagar correctamente el gradiente a través de la última etapa de la pasada directa.

El siguiente paso clave consiste en llamar al método AttentNormGrad, que implementa las funciones de servicio del kernel homónimo del programa OpenCL. Aquí es importante tener debidamente en cuenta la influencia de todas las transformaciones estadísticas aplicadas anteriormente, así como la de los pesos de atención, para garantizar una propagación coherente del error.

   if(!AttentNormGrad(cTranspose.AsObject()))
      return false;

A continuación, se invoca el método CalcHiddenGradients del objeto cAttention, lo que permite trasladar el error desde el nivel de los pesos probabilísticos de la función SoftMax hasta los coeficientes de atención. Esto es importante, ya que los pesos de atención, al ser una función de varias variables de entrada, requieren un tratamiento especial a la hora de calcular el gradiente.

   if(!cAttention.CalcHiddenGradients(cSoftMax.AsObject()))
      return false;
   if(!MatMulGrad(NeuronOCL.getOutput(), PrevOutput,
                  cTranspose.getOutput(), cTranspose.getPrevOutput(),
                  cAttention.getGradient(),
                  iWindow, iCount, iWindow, 1, false))
      return false;

Luego se llama a la función MatMulGrad, que realiza la operación inversa a la multiplicación de matrices aplicada en la pasada directa. Aquí se tienen en cuenta los datos de entrada y su copia transpuesta. Esto permite reconstruir los gradientes con precisión hasta el nivel de las interrelaciones internas entre los elementos de la ventana analizada.

Sin embargo, hay que tener en cuenta que, para el nivel de los datos transpuestos, ya hemos obtenido el gradiente de error por la ruta principal del flujo de normalización de los datos. Además, aún tenemos que transferir los datos al nivel de datos de entrada desde el objeto de transposición. Por eso, en esta fase, guardamos los gradientes de error obtenidos en búferes auxiliares de datos.

En el siguiente paso, sumamos los gradientes de error obtenidos a partir de los dos flujos de información a nivel del objeto de transposición.

   if(!SumAndNormilize(cTranspose.getGradient(), cTranspose.getPrevOutput(), cTranspose.getGradient(),
                       iWindow, false, 0, 0, 0, 1))
      return false;

Luego transmitimos los valores obtenidos en la representación de entrada, donde también sumamos los valores de los dos flujos de información.

   if(!NeuronOCL.CalcHiddenGradients(cTranspose.AsObject()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), getPrevOutput(), NeuronOCL.getGradient(),
                       iCount, false, 0, 0, 0, 1))
      return false;
//---
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(),
                       NeuronOCL.getGradient(), NeuronOCL.Activation()))
         return false;
//---
   return true;
  }

Por último, si se ha aplicado una función de activación a los datos de entrada, se calcula su derivada mediante el método DeActivation. Esto cierra la cadena de propagación del gradiente, lo que garantiza que la señal de error recorra completamente todas las etapas de la transformación en sentido inverso.

Cabe destacar especialmente que todo el algoritmo implementado para la extracción de la componente espacial, teniendo en cuenta la atención y la normalización, no contiene parámetros entrenables en el sentido habitual: es decir, ni pesos, ni sesgos, ni coeficientes internos que requieran optimización mediante descenso por gradiente. Todos los cálculos se basan exclusivamente en los valores actuales de los datos de entrada, en las estadísticas de la muestra y en la función SoftMax, que genera la distribución de atención.

Precisamente por eso, el método updateInputWeights, encargado de actualizar los parámetros del modelo tras cada pasada, no se redefine en este caso. En su lugar, se conserva la implementación stub heredada de la clase padre. Esta solución se ajusta a la arquitectura general del proyecto: no aumenta la complejidad del modelo, no requiere cálculos adicionales y pone de relieve el carácter auxiliar y estructural de esta capa en el contexto de todo el sistema de red neuronal.

El código completo de esta clase y de todos sus métodos se puede consultar en el archivo adjunto. Su estructura pone de relieve el carácter bien meditado de la solución arquitectónica, así como su estricta vinculación con los componentes ya implementados anteriormente, lo que facilita el escalado y la reutilización del módulo dentro de esquemas de redes neuronales más complejos.


Módulo de regresión polinómica

El siguiente componente que tenemos que implementar es el módulo de regresión polinómica. Los autores del framework SSCNN han ampliado considerablemente la arquitectura del componente, añadiendo interacciones multiplicativas a las conexiones aditivas. Esta ampliación permitió implementar formas más complejas y expresivas de interdependencia entre los componentes de entrada del modelo.

¿Para qué sirve esto? Las series temporales financieras rara vez siguen patrones estrictamente lineales o simplemente aditivos. En realidad, nos enfrentamos a un entrelazamiento de tendencias, ciclos, estacionalidad y ruido, entre los que a menudo se manifiestan relaciones complejas y no lineales. El modelo clásico de regresión polinómica, incluso con un gran número de grados de libertad, a menudo resulta insuficientemente flexible para describir adecuadamente dichos efectos.

Para captar adecuadamente las interacciones de segundo orden (e incluso de orden superior), se utiliza el procesamiento paralelo de la señal de entrada a través de dos rutas:

  • una ruta aditiva, implementada mediante una capa convolucional, encargada de captar las dependencias lineales y débilmente no lineales entre características que actúan de forma independiente;
  • una ruta multiplicativa, representada por un bloque de capas convolucionales. Es precisamente aquí donde se implementa la idea de modulación dependiente del contexto de las características, lo que permite tener en cuenta combinaciones no lineales y el refuerzo o la atenuación mutuos de los componentes.

En nuestra implementación hemos ido aún más allá, añadiendo a la ruta multiplicativa la activación SwiGLU: una de las no linealidades modernas y eficaces que contribuyen a aumentar la capacidad de aprendizaje del modelo sin un crecimiento significativo de los parámetros.

El objeto CNeuronPolynomialRegression implementa una capa ampliada de regresión polinómica y forma parte de la pila computacional de un modelo de red neuronal orientado al pronóstico de series temporales en el entorno OpenCL. Hereda las interfaces principales de la clase base CNeuronBaseOCL, lo que garantiza la compatibilidad con los demás componentes del framework sin necesidad de adaptación. El diseño de esta capa se basa en principios de composición: cada bloque interno se encarga de una tarea claramente definida, y la interacción entre ellos forma un sistema complejo pero flexible de procesamiento de la señal de entrada.

class CNeuronPolynomialRegression   :  public CNeuronBaseOCL
  {
protected:
   CNeuronSwiGLUOCL     cProjection;
   CNeuronConvOCL       cConvolution;
   CNeuronConvOCL       cResidual;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronPolynomialRegression(void) {};
                    ~CNeuronPolynomialRegression(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint window, uint window_out,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronPolynomialRegression; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual uint      GetWindowIn(void) const { return cProjection.GetWindow(); }
   virtual uint      GetWindowOut(void) const { return cResidual.GetFilters(); }
   virtual uint      GetUnits(void) const { return cProjection.GetUnits(); }
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { }
  };

Dentro de la clase se incluyen tres componentes clave: el bloque de proyección cProjection, basado en el mecanismo SwiGLU, actúa como el primer transformador de datos. Expande las características y las prepara para su posterior procesamiento. Además, se utilizan dos capas convolucionales, diseñadas para captar dependencias residuales y multiplicativas. Esta separación permite combinar relaciones tanto aditivas como polinómicas (de segundo orden y superiores) entre las características, ampliando así la capacidad expresiva del modelo sin complicar excesivamente su estructura.

La arquitectura interna de la clase está organizada de tal manera que todos los componentes que la conforman se declaran como miembros estáticos. Esto significa que se crean una sola vez durante la inicialización y no requieren una asignación dinámica de memoria ni una gestión manual de su ciclo de vida. Este enfoque garantiza la simplicidad del diseño, reduce la sobrecarga y evita la necesidad de definir explícitamente el constructor y el destructor de la clase; estos métodos permanecen vacíos, lo que resalta la concisión y la autonomía de la estructura.

La configuración de toda la arquitectura interna se lleva a cabo en el método Init, que efectivamente inicializa el grafo computacional de la capa.

bool CNeuronPolynomialRegression::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                       uint units_count, uint window, uint window_out,
                                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_count * window_out,
                                                         optimization_type, batch))
      return false;
   activation = None;

El primer paso consiste en llamar al método de inicialización base de la clase padre. Esta llamada constituye la base para la asignación de memoria y la vinculación de los búferes de cálculo. A continuación, la función de activación de la capa se establece explícitamente en None, ya que su implementación está integrada en la estructura de los bloques subordinados.

A continuación, se lleva a cabo la inicialización secuencial de cada componente de la capa. El módulo cProjection se configura como la primera etapa del procesamiento de la señal: recibe los datos de entrada y los expande según el número de unidades y el tamaño de la ventana especificados. Su configuración determina la capacidad de procesamiento de la capa y sienta las bases para la regresión posterior.

   if(!cProjection.Init(0, 0, OpenCL, window, window, window_out, units_count, 1,
                                                            optimization, iBatch))
      return false;
   if(!cConvolution.Init(0, 1, OpenCL, window_out, window_out, window_out, units_count,
                                                              1, optimization, iBatch))
      return false;
   cConvolution.SetActivationFunction(None);

Después se inicializa cConvolution, que se encarga de las relaciones polinómicas. Se le pasa la salida del bloque de proyección, y sus dimensiones corresponden a la ventana de resultados. La función de activación de este componente se desactiva, ya que se utiliza una convolución pura sin distorsión de la señal.

La etapa final es el objeto cResidual. También procesa los datos de entrada, pero realiza una convolución convencional sin multiplicación. De este modo se modela la parte lineal.

   if(!cResidual.Init(0, 2, OpenCL, window, window, window_out, units_count, 1, optimization, iBatch))
      return false;
   cResidual.SetActivationFunction(None);

Tras inicializar correctamente los tres componentes, se lleva a cabo la sincronización de los búferes de gradientes entre las capas. El gradiente de cConvolution se establece como principal para la capa actual y para cResidual. Esto garantiza una dirección común para ajustar el modelo sin copias de datos innecesarias.

   if(!SetGradient(cConvolution.getGradient(), true))
      return false;
   if(!cResidual.SetGradient(getGradient(), true))
      return false;
//---
   return true;
  }

Si todas las etapas de la inicialización se completan sin errores, el método devuelve true, lo que indica que la capa está lista para funcionar. De este modo, todo el proceso de configuración está estrictamente centralizado, lo que hace que el comportamiento del componente sea predecible, controlable y robusto frente a errores en otras partes del sistema.

Ahora que ya hemos dejado atrás los métodos de inicialización, pasemos al corazón de los cálculos: la pasada directa en el método feedForward.

bool CNeuronPolynomialRegression::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cProjection.FeedForward(NeuronOCL))
      return false;
   if(!cConvolution.FeedForward(cProjection.AsObject()))
      return false;

En primer lugar, delegamos el procesamiento de los datos de entrada al módulo de proyección cProjection, pasándole un puntero al objeto de la capa neuronal anterior. Si en esta fase algo sale mal, la función terminará inmediatamente su ejecución con el resultado false.

Si todo va bien, el resultado de la transformación SwiGLU se envía al bloque convolucional cConvolution, donde se genera una contribución cuadrática: aquí se vuelve a comprobar que la operación se haya realizado correctamente y, en caso de error, el método detiene su ejecución.

A continuación, esa misma información de entrada se transmite al bloque lineal cResidual, que se encarga de conservar la parte puramente lineal de la señal.

   if(!cResidual.FeedForward(NeuronOCL))
      return false;
   if(!SumAndNormilize(cConvolution.getOutput(), cResidual.getOutput(),
                       Output, GetWindowOut(), true, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

Cuando los tres componentes se han ejecutado correctamente, sus salidas se transmiten a la función SumAndNormilize, donde se realiza la suma elemento a elemento de las ramas y la normalización según el tamaño de la ventana. Guardamos los valores obtenidos en el búfer de resultados de la interfaz externa. Así, sin una sola pausa innecesaria, cada paso fluye con naturalidad hacia el siguiente, lo que garantiza un funcionamiento coordinado y fiable de la capa polinómica.

Tras una pasada directa correcta, comienza la retropropagación del error en el método calcInputGradients.

bool CNeuronPolynomialRegression::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

Se comprueba inmediatamente la validez del puntero al objeto de datos de entrada. Si es nulo, la función devuelve false sin realizar ninguna acción adicional.

A continuación, el gradiente actual del bloque convolucional se transmite al módulo de proyección.

   if(!cProjection.CalcHiddenGradients(cConvolution.AsObject()))
      return false;
//---
   if(!NeuronOCL.CalcHiddenGradients(cProjection.AsObject()))
      return false;

Sin interrupciones, el método accede directamente al propio objeto de datos de entrada, llamando al método CalcHiddenGradients, lo que permite propagar los gradientes hacia abajo hasta el nivel correspondiente.

Una vez finalizada esta parte, pasamos a la propagación del gradiente del error a través del segundo flujo de información. Pero antes guardamos el puntero al búfer actual en la variable temporal temp para no perder los valores acumulados. Para procesar el componente residual (lineal), sustituimos el puntero al búfer de gradientes en el objeto de datos de entrada por un búfer libre y solo entonces volvemos a llamar al método CalcHiddenGradients, repartiendo la parte lineal del gradiente del error.

   CBufferFloat* temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(NeuronOCL.getPrevOutput(), false) ||
      !NeuronOCL.CalcHiddenGradients(cResidual.AsObject()) ||
      !SumAndNormilize(temp, NeuronOCL.getGradient(), temp,
                       GetWindowIn(), false, 0, 0, 0, 1) ||
      !NeuronOCL.SetGradient(temp, false))
      return false;
//---
   return true;
  }

A continuación, sumamos los dos flujos de gradientes. Por último, el gradiente combinado se vuelve a establecer en la neurona mediante el método SetGradient. Si ninguna de las operaciones ha fallado, el método finaliza devolviendo true, y toda la tupla de flujos de gradientes se ha propagado con éxito hacia atrás, lo que garantiza el ajuste correcto de los pesos en la capa polinómica.

Ahora que los gradientes se han recopilado y están listos para aplicarse, pasamos de forma natural a la fase de actualización de los pesos en el método updateInputWeights. Aquí, cada componente vuelve a funcionar por turnos: primero, el módulo de proyección cProjection ajusta sus parámetros internos y, a continuación, el control pasa de forma fluida al bloque cConvolution.

bool CNeuronPolynomialRegression::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cProjection.UpdateInputWeights(NeuronOCL))
      return false;
   if(!cConvolution.UpdateInputWeights(cProjection.AsObject()))
      return false;
   if(!cResidual.UpdateInputWeights(NeuronOCL))
      return false;
//---
   return true;
  }

Por último, el bloque lineal cResidual cierra la cadena, actualizando sus pesos a partir de los datos de entrada y de los gradientes de error obtenidos anteriormente. Si se produce un error en cualquiera de estas tres etapas, el método devuelve inmediatamente false; pero, si cada ajuste se lleva a cabo sin errores, updateInputWeights finaliza su ejecución sin contratiempos, devolviendo true y garantizando que todos los componentes de CNeuronPolynomialRegression queden sincronizados y listos para el siguiente ciclo de entrenamiento.

El código completo de la clase y de todos sus métodos se incluye en el archivo adjunto.

Hoy hemos logrado un avance significativo: se han creado con éxito tres componentes clave del framework SSCNN. Ahora es el momento de tomarse un respiro para que los nuevos conocimientos y observaciones se asienten en la mente, como el rocío al amanecer. Una vez que hayamos descansado, pasaremos a la siguiente fase con la mente despejada y energía renovada.

En el próximo artículo, conectaremos todos los módulos en una única cadena de cálculos y los someteremos a una auténtica prueba de campo: ejecutaremos el algoritmo con datos históricos del mercado para evaluar su robustez y precisión. En este terreno de prueba real, comprobaremos hasta qué punto nuestras soluciones funcionan de forma elegante y eficaz. Y, por supuesto, extraeremos conclusiones que nos ayudarán a hacer que SSCNN sea aún más robusto y tenga mayor rendimiento.


Conclusión

El paso de la descripción conceptual del framework SSCNN a su implementación demostró la importancia de la estructura y la coherencia en todos los niveles de construcción de modelos orientados al análisis de series temporales de mercado. Los autores del framework han rechazado deliberadamente el escalado como enfoque universal, en favor de la descomposición y la modularidad, un principio que permite mantener la interpretabilidad, la adaptabilidad y la robustez del modelo ante un alto nivel de ruido del mercado.

El diseño de CNeuronAttentNorm se convirtió en la expresión más acabada de nuestro enfoque: combina mecanismos de atención y normalización con gran carga semántica, implementados en la GPU teniendo en cuenta el contexto del segmento y las características de las señales financieras. El enfoque para la inicialización, la transferencia y la sincronización de datos entre la CPU y la GPU se diseñó para evitar los típicos cuellos de botella asociados al cálculo paralelo en OpenCL.

Se prestó especial atención al entrenamiento de los parámetros de atención, lo que permite al modelo concentrarse de forma selectiva en los tramos más significativos de la señal analizada. No se trata solo de mejorar la precisión del pronóstico, sino también de dar un paso hacia una generalización más significativa, en la que cada componente del modelo —desde la descomposición estructural hasta la normalización— funciona en sintonía con el contexto.

En el próximo artículo, integraremos todos los componentes en un único algoritmo y realizaremos una prueba de campo sobre datos financieros históricos. Esto permitirá evaluar la robustez y la precisión de SSCNN en condiciones reales.

Referencias


Programas utilizados en el artículo

# Nombre Tipo Descripción
1 Study.mq5 Asesor experto Asesor experto para el entrenamiento offline de modelos
2 StudyOnline.mq5 Asesor experto Asesor experto para el entrenamiento online de modelos
3 Test.mq5 Asesor experto Asesor experto para probar el modelo
4 Trajectory.mqh Biblioteca de clases Estructura para describir el estado del sistema y la arquitectura de los modelos
5 NeuroNet.mqh Biblioteca de clases Biblioteca de clases para crear una red neuronal
6 NeuroNet.cl Biblioteca Biblioteca de código del programa OpenCL

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

Archivos adjuntos |
MQL5.zip (2963.02 KB)
Herramientas de trading de MQL5 (Parte 4): Mejorando el panel del escáner multitemporal con posicionamiento dinámico y funciones de alternancia Herramientas de trading de MQL5 (Parte 4): Mejorando el panel del escáner multitemporal con posicionamiento dinámico y funciones de alternancia
En este artículo, actualizamos el panel de control del escáner multitemporal MQL5 con funciones de arrastre y alternancia. Hemos habilitado la opción de arrastrar el panel de control y una opción para minimizar/maximizar que permite un mejor uso de la pantalla. Implementamos y probamos estas mejoras para lograr una mayor flexibilidad operativa en el trading.
Implementación de un circuito cuántico para Quantum Reservoir Computing (QRC) Implementación de un circuito cuántico para Quantum Reservoir Computing (QRC)
Hoy hablaremos de un enfoque revolucionario del aprendizaje automático en el trading mediante la computación cuántica. El artículo muestra la implementación práctica de un sistema QRC adaptativo con aprendizaje adicional continuo para predecir los movimientos del mercado en tiempo real.
Redes neuronales en el trading: descomposición en lugar de escalado (Final) Redes neuronales en el trading: descomposición en lugar de escalado (Final)
Le proponemos familiarizarse con el algoritmo para descomponer una serie temporal en capas semánticas y construir a partir de ellas un modelo eficiente. Mostramos paso a paso la arquitectura, la implementación práctica en MQL5/OpenCL y las pruebas reales sobre datos históricos de mercado.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 31): Motor de reconocimiento de velas japonesas en Python (I) — Detección manual Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 31): Motor de reconocimiento de velas japonesas en Python (I) — Detección manual
Los patrones de velas japonesas son fundamentales para el trading basado en la evolución de los precios, ya que ofrecen información valiosa sobre posibles reversiones o continuaciones del mercado. Imagina una herramienta fiable que supervise continuamente cada nueva barra de precios, identifique formaciones clave como los patrones envolvente, martillo, doji y estrella, y te avise de inmediato cuando detecte una oportunidad de negociación significativa. Esta es precisamente la funcionalidad que hemos desarrollado. Tanto si eres nuevo en el mundo del trading como si eres un profesional con experiencia, este sistema ofrece alertas en tiempo real sobre patrones de velas japonesas, lo que te permite centrarte en ejecutar operaciones con mayor confianza y eficiencia. Sigue leyendo para descubrir cómo funciona y cómo puede mejorar tu estrategia de trading.