English Русский 中文 Português
preview
Redes neuronales en el trading: Aprendizaje multitarea basado en el modelo ResNeXt

Redes neuronales en el trading: Aprendizaje multitarea basado en el modelo ResNeXt

MetaTrader 5Sistemas comerciales |
193 9
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

El rápido desarrollo de la inteligencia artificial ha llevado a la adopción activa de técnicas de aprendizaje profundo en el análisis de datos, incluido el sector financiero. Los datos financieros poseen una gran dimensionalidad, heterogeneidad y estructura temporal, lo que dificulta la aplicación de los métodos de procesamiento tradicionales. Al mismo tiempo, el aprendizaje profundo ha demostrado ser muy eficaz en el análisis de datos complejos y no estructurados.

Entre las arquitecturas modernas de modelos convolucionales, llama la atención ResNeXt, presentada en el artículo: "Aggregated Residual Transformations for Deep Neural Networks". ResNeXt demuestra la capacidad de detectar dependencias locales y globales, así como de manejar eficazmente datos multidimensionales, reduciendo la complejidad computacional mediante la convolución de grupos.

Una de las áreas clave del análisis financiero basado en el aprendizaje profundo es el aprendizaje multitarea (Multi-Task Learning, MTL). Este planteamiento permite resolver simultáneamente varios problemas relacionados, lo cual mejora la precisión de los modelos y su capacidad de generalización. A diferencia del enfoque clásico, en el que cada modelo resuelve un problema por separado, el aprendizaje multitarea usa representaciones conjuntas de los datos, lo que hace que el modelo resulte más robusto ante los cambios del mercado y mejora el proceso de aprendizaje. Este enfoque resulta especialmente útil para prever las tendencias del mercado, evaluar el riesgo y determinar el valor de los activos, ya que los mercados financieros son dinámicos y dependen de muchos factores.

En el artículo: "Collaborative Optimization in Financial Data Mining Through Deep Learning and ResNeXt" se presentó un framework de iteración de la estructura ResNeXt en modelos multitarea. La solución mostrada abre nuevas oportunidades en el procesamiento de series temporales, la identificación de patrones espaciotemporales y la elaboración de previsiones precisas. La convolución grupal y los bloques residuales de ResNeXt aumentan la velocidad de aprendizaje y reducen la probabilidad de perder características importantes, lo que hace que este método sea especialmente relevante para el análisis financiero.

Otra ventaja importante del enfoque propuesto es que automatiza la extracción de características significativas de los datos de origen. Los métodos tradicionales de análisis de la información financiera requieren una compleja ingeniería de características, mientras que los modelos de redes neuronales profundas son capaces de extraer patrones clave de forma independiente. Esto resulta especialmente relevante cuando se analizan datos financieros multimodales en los que hay que tener en cuenta varias fuentes de información, incluidos los indicadores de mercado, los informes macroeconómicos y las publicaciones de noticias. La flexibilidad del aprendizaje multitarea permite cambios dinámicos en las ponderaciones de las tareas y las funciones de pérdida, lo cual aumenta la adaptabilidad del modelo a los cambios en el entorno del mercado y mejora la precisión de las previsiones.


La arquitectura ResNeXt

La arquitectura ResNeXt utiliza la modularidad y las convoluciones grupales. Se basa en bloques convolucionales con conexiones residuales que obedecen a dos reglas clave:

  • Si los mapas de características de salida tienen el mismo tamaño, los bloques usan hiperparámetros idénticos (anchuras y tamaños de filtro);
  • Si el tamaño de los mapas disminuye, la anchura de los bloques aumenta proporcionalmente.

Seguir estos principios permite mantener a un nivel aproximadamente constante la complejidad computacional en todos los niveles del modelo, lo que simplifica enormemente el proceso de su diseño. Basta con definir un módulo de plantilla y el resto de los bloques se generan automáticamente, lo cual posibilita la normalización, simplificando la configuración y el análisis de la arquitectura.

Las neuronas convencionales de las redes neuronales artificiales realizan una suma ponderada de los valores de entrada, que es la operación básica en las capas convolucionales y las capas totalmente conectadas. Este proceso puede dividirse en tres etapas principales: partición, transformación y agregación. Sin embargo, ResNeXt introduce un enfoque más flexible en lugar de la transformación habitual; en este, las funciones de transformación pueden ser más complejas e incluso representar minimodelos. Esto nos lleva al concepto de "red en neurona" (Network-in-Neuron), que amplía las capacidades de la arquitectura a través de una nueva dimensión: el número de canales (cardinality). A diferencia de la anchura o la profundidad de un modelo, el número de canales determina el número de transformaciones complejas independientes en cada bloque. Los estudios experimentales demuestran que el aumento de este parámetro puede ser más eficaz que aumentar la profundidad o la anchura de la red, ya que ofrece un mejor equilibrio entre rendimiento y eficiencia computacional.

Todos los bloques de ResNeXt tienen la misma estructura usando el módulo de cuello de botella (bottleneck). Este consta de:

  • una capa inicial de convolución 1×1 que reduce la dimensionalidad de las características,
  • una capa principal de convolución 3×3, que realiza el procesamiento principal de los datos,
  • una última capa de convolución 1×1, que retorna la dimensionalidad original.

Este enfoque reduce la complejidad computacional al tiempo que mantiene un modelo altamente expresivo. Además, el uso de enlaces residuales ayuda a mantener los gradientes durante el entrenamiento, evitando que se desvanezcan, lo cual resulta clave para los modelos profundos.

Una de las principales mejoras de ResNeXt es el uso de convoluciones grupales (grouped convolutions). En este enfoque, los datos de entrada se dividen en varios grupos independientes, cada uno de los cuales es procesado por un filtro de convolución distinto, después de lo cual se combinan los resultados. Este mecanismo reduce el número de parámetros del modelo, mantiene un gran ancho de banda de la red y mejora la eficiencia computacional sin pérdida significativa de precisión.

Para garantizar la estabilidad de la complejidad computacional a medida que cambia el número de grupos, ResNeXt adapta la anchura de los bloques de bottleneck ajustando el número de canales en las capas internas. Esto permite escalar el modelo sin aumentos excesivos del coste computacional.

El marco de aprendizaje multitarea basado en ResNeXt representa un enfoque progresivo del procesamiento de datos financieros, que aborda el problema de la compartición de características y el modelado cooperativo para diversas tareas analíticas. Se basa en tres componentes estructurales clave:

  • el módulo de extracción de características;
  • el módulo de aprendizaje colaborativo;
  • las capas de salida especializadas para cada tarea.

Este enfoque nos permite integrar mecanismos eficientes de aprendizaje profundo con las series temporales financieras, garantizando una alta precisión de previsión y la adaptación del modelo a las condiciones cambiantes del mercado.

El módulo de extracción de características se basa en la arquitectura ResNeXt, que permite una extracción eficaz de las características de los datos financieros, tanto locales como globales. Al procesar datos financieros multidimensionales, el número de grupos en la arquitectura del modelo cumple un papel especial. Este hiperparámetro permite encontrar el equilibrio óptimo entre la representación detallada de las características y el coste computacional. Cada operación convolucional grupal en ResNeXt identifica patrones específicos en diferentes grupos de canales y luego los agrega en una única representación.

Tras pasar por las capas de transformaciones no lineales, las características generadas se convierten en la base para el posterior entrenamiento multitarea y la adaptación específica de la tarea del modelo. El módulo de entrenamiento conjunto integra un mecanismo de reparto de pesos que permite proyectar características comunes en el espacio de tareas específicas. Este mecanismo garantiza que el modelo sea capaz de extraer representaciones individuales para cada tarea, eliminando la interferencia mutua entre tareas y asegurando además un alto grado de compartición de características. La división de tareas en clústeres, considerando las correlaciones entre ellas, aumenta aún más la eficacia del mecanismo de aprendizaje conjunto.

Las capas resultantes de cada tarea son perceptrones totalmente conectados que ejecutan la proyección de características especializadas en el espacio de predicción final. Las capas de salida pueden adaptarse según las especificidades de las tareas a resolver. En concreto, la función de pérdida basada en la entropía cruzada se utiliza en tareas de clasificación, mientras que el error cuadrático medio (MSE) se utiliza en tareas de regresión. Para el entrenamiento conjunto de todas las tareas, la función de pérdida resultante se representa como una suma ponderada de las pérdidas de las tareas individuales.

El proceso de entrenamiento del modelo se realiza en varias etapas. En primer lugar, se efectúa un entrenamiento previo de los modelos para problemas individuales con el fin de garantizar una convergencia eficaz de los MLP especializados. A continuación, el modelo se preentrena en una arquitectura multitarea, lo que contribuye a mejorar su rendimiento global. La optimización se realiza usando el algoritmo Adam con variación dinámica de la tasa de aprendizaje.



Implementación con MQL5

Tras considerar los aspectos teóricos del marco de aprendizaje multitarea basado en ResNeXt, procedemos a la implementación de nuestra propia visión de los enfoques propuestos mediante MQL5. Y comenzaremos nuestro trabajo construyendo los bloques básicos de la arquitectura ResNeXt: los módulos bottleneck

Módulo cuello de botella


Como ya hemos mencionado, el módulo de bottleneck consta de tres capas convolucionales, cada una de las cuales cumple una función clave en el procesamiento de los datos de origen. La primera capa se encarga de reducir la dimensionalidad del espacio de características, lo cual reduce la complejidad computacional del posterior procesamiento de la información.

La segunda capa de convolución realiza una convolución básica para extraer características complejas de alto nivel necesarias para una interpretación precisa de los datos de origen. Asimismo, analiza las relaciones entre los distintos elementos de los datos, identificando patrones que pueden resultar críticos para las fases posteriores del análisis. Este enfoque permite al modelo adaptarse a las dependencias no lineales de los datos financieros, lo cual mejora la precisión de las previsiones.

La última capa se encarga de restaurar la dimensionalidad original del tensor de datos, cosa fundamental para preservar toda la información significativa. En el paso anterior de extracción de características, se puede reducir la dimensionalidad del tensor a lo largo del eje de pasos temporales. Esto se compensa aumentando la dimensionalidad del espacio de características, lo cual resulta coherente con los principios arquitectónicos de ResNeXt.

Para estabilizar el proceso de aprendizaje, se aplica una normalización por lotes tras cada capa convolucional. Esto reduce el desplazamiento de covarianza interna y acelera la convergencia del modelo. Como función de activación, se utiliza una ReLU, lo que aumenta la no linealidad del modelo, mejorando su capacidad para identificar dependencias complejas en los datos y mejorando la calidad de la generalización.

La arquitectura descrita anteriormente la implementaremos dentro del objeto CNeuronResNeXtBottleneck, cuya estructura se muestra a continuación.

class CNeuronResNeXtBottleneck :  public CNeuronConvOCL
  {
protected:
   CNeuronConvOCL          cProjectionIn;
   CNeuronBatchNormOCL     cNormalizeIn;
   CNeuronTransposeRCDOCL  cTransposeIn;
   CNeuronConvOCL          cFeatureExtraction;
   CNeuronBatchNormOCL     cNormalizeFeature;
   CNeuronTransposeRCDOCL  cTransposeOut;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronResNeXtBottleneck(void){};
                    ~CNeuronResNeXtBottleneck(void){};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint chanels_in, uint chanels_out, uint window,
                          uint step, uint units_count, uint group_size, uint groups,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronResNeXtBottleneck;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual CLayerDescription* GetLayerInfo(void) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

Para ello, como clase padre usamos un objeto de capa convolucional que realizará la función de reconstrucción del espacio de características. Además, en la estructura presentada, podemos ver una serie de objetos internos que cumplirán funciones clave en los algoritmos que estamos construyendo. Aprenderemos más sobre su funcionalidad durante la construcción de los métodos de la nueva clase.

Todos los objetos internos se declararán estáticamente, lo que nos permitirá dejar vacíos el constructor y el destructor de la clase, mientras que la inicialización de todos los objetos declarados y heredados se realizará en el método Init. En los parámetros del método especificado obtenemos una serie de constantes que ofrecen una idea inequívoca de la arquitectura del módulo creado.

bool CNeuronResNeXtBottleneck::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint chanels_in, uint chanels_out, uint window,
                                   uint step, uint units_count, uint group_size, uint groups,
                                   ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   int units_out = ((int)units_count - (int)window + (int)step - 1) / (int)step + 1;
   if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, group_size * groups, group_size * groups,
                                              chanels_out, units_out, 1, optimization_type, batch))
      return false;

En el cuerpo del método, solemos llamar directamente al método homónimo de la clase padre, que ya implementa los algoritmos para inicializar los objetos heredados y las interfaces. Sin embargo, en este caso, tenga en cuenta que la clase padre actúa como la última capa convolucional del módulo. A su entrada se suministran los datos tras la extracción de características, lo que puede haber cambiado la dimensionalidad del tensor procesado. Por lo tanto, primero determinamos la longitud de la secuencia a la salida del módulo, y solo entonces llamamos al método de la clase padre.

Una vez inicializados correctamente los objetos heredados, pasamos a trabajar con los objetos recién declarados. Aquí empezaremos nuestro trabajo con el bloque de proyección de datos. Con la primera capa convolucional, preparamos las proyecciones de datos de origen para el número requerido de grupos de trabajo.

//--- Projection In
   uint index = 0;
   if(!cProjectionIn.Init(0, index, OpenCL, chanels_in, chanels_in, group_size * groups,
                                                    units_count, 1, optimization, iBatch))
      return false;
   index++;
   if(!cNormalizeIn.Init(0, index, OpenCL, cProjectionIn.Neurons(), iBatch, optimization))
      return false;
   cNormalizeIn.SetActivationFunction(LReLU);

Luego normalizamos las proyecciones resultantes y añadimos una LReLU como función de activación.

Cabe señalar que como resultado de estas operaciones obtenemos datos en forma de tensor tridimensional [Time, Group, Dimеnsion]. Para construir un algoritmo para el procesamiento independiente de grupos individuales, sacaremos la dimensionalidad del identificador de grupo al primer lugar utilizando el objeto de transposición del tensor tridimensional.

   index++;
   if(!cTransposeIn.Init(0, index, OpenCL, units_count, groups, group_size, optimization, iBatch))
      return false;
   cTransposeIn.SetActivationFunction((ENUM_ACTIVATION)cNormalizeIn.Activation());

A continuación viene el bloque de extracción de características. En él, utilizamos una capa de convolución con el número de grupos como número de secuencias independientes. Esto permite "desmezclar" los valores de los distintos grupos. Al mismo tiempo, utilizamos una matriz diferente de parámetros entrenados para cada grupo.

//--- Feature Extraction
   index++;
   if(!cFeatureExtraction.Init(0, index, OpenCL, group_size * window, group_size * step, group_size,
                                                           units_out, groups, optimization, iBatch))
      return false;

También debemos considerar que en los parámetros del método obtenemos el tamaño de la ventana de convolución y su paso en la dimensión de pasos temporales. Por ello, al pasar los parámetros al método de inicialización de la capa de convolución interna, multiplicamos los parámetros correspondientes por el tamaño del grupo.

Tras la capa de convolución, añadimos la normalización por lotes con la función de activación LReLU.

   index++;
   if(!cNormalizeFeature.Init(0, index, OpenCL, cFeatureExtraction.Neurons(), iBatch, optimization))
      return false;
   cNormalizeFeature.SetActivationFunction(LReLU);

El último bloque de la proyección inversa del espacio de características solo incluye un objeto de transposición tensorial 3D que combina los grupos en una única secuencia. La proyección directa de los datos, como ya hemos mencionado, se realiza usando las herramientas heredadas de la clase padre.

//--- Projection Out
   index++;
   if(!cTransposeOut.Init(0, index, OpenCL, groups, units_out, group_size, optimization, iBatch))
      return false;
   cTransposeOut.SetActivationFunction((ENUM_ACTIVATION)cNormalizeFeature.Activation());
//---
   return true;
  }

Todo lo que debemos hacer es devolver el resultado lógico de las operaciones al programa que realiza la llamada y finalizar el método de inicialización del objeto.

La siguiente etapa de nuestro trabajo consiste en construir un algoritmo de pasada directa que implementaremos en el método feedForward.

bool CNeuronResNeXtBottleneck::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Projection In
   if(!cProjectionIn.FeedForward(NeuronOCL))
      return false;

En los parámetros del método obtenemos el puntero al objeto de datos de origen, que pasamos directamente al método homónimo de la primera capa de convolución interna de proyección de datos. No vamos a comprobar la relevancia del puntero obtenido, ya que esta comprobación se crea ya en el método de la capa interna, y en este caso dicho control será innecesario.

Luego normalizamos los resultados de la proyección y los transponemos a una representación de los grupos individuales.

   if(!cNormalizeIn.FeedForward(cProjectionIn.AsObject()))
      return false;
   if(!cTransposeIn.FeedForward(cNormalizeIn.AsObject()))
      return false;

En el bloque de extracción de características, realizamos las operaciones de convolución grupal y normalizamos los resultados obtenidos.

//--- Feature Extraction
   if(!cFeatureExtraction.FeedForward(cTransposeIn.AsObject()))
      return false;
   if(!cNormalizeFeature.FeedForward(cFeatureExtraction.AsObject()))
      return false;

A continuación, trasladamos las características extraídas de los grupos individuales a una única secuencia multidimensional y las proyectamos en el espacio de características establecido por medio de la clase padre.

//--- Projection Out
   if(!cTransposeOut.FeedForward(cNormalizeFeature.AsObject()))
      return false;
   return CNeuronConvOCL::feedForward(cTransposeOut.AsObject());
  }

Después retornamos el resultado lógico de las operaciones al programa que realiza la llamada y finalizamos el método.

Como habrá notado, el algoritmo del método de pasada directa es de naturaleza lineal. Y también el gradiente de error se propaga linealmente. Por ello, le sugiero estudiar los métodos de pasada inversa por su propia cuenta. Encontrará el código completo del objeto presentado y todos sus métodos en el archivo adjunto.

Módulo residual


La arquitectura ResNeXt se caracteriza por la presencia de enlaces residuales para cada módulo de bottleneck, lo que contribuye a una propagación más eficiente del gradiente de error durante la pasada inversa. Estas relaciones permiten al modelo reutilizar características extraídas previamente, lo cual mejora la convergencia y reduce el riesgo de desvanecimiento del gradiente. Como resultado, el modelo puede entrenarse a un nivel más profundo sin un aumento significativo del coste computacional.

Debemos señalar que en la salida del módulo de bottleneck se genera un tensor cuyo tamaño global sigue siendo aproximadamente el mismo, pero sus dimensiones individuales cambian. La disminución del número de pasos temporales se compensa con un aumento de la dimensionalidad del espacio de características, lo cual permite al modelo retener información clave y considerar las dependencias a largo plazo de los datos. Para organizar correctamente el flujo de enlaces residuales, se ofrece un módulo especial de proyección de datos de origen en las dimensionalidades correspondientes, que garantiza la correcta integración de la información entre las capas del modelo. Esto evita problemas de desajuste dimensional y mantiene la estabilidad del aprendizaje incluso en arquitecturas profundas.

Como parte de nuestro artículo, hemos creado un módulo de este tipo en forma de objeto CNeuronResNeXtResidual, cuya estructura se muestra a continuación.

class CNeuronResNeXtResidual:  public CNeuronConvOCL
  {
protected:
   CNeuronTransposeOCL     cTransposeIn;
   CNeuronConvOCL          cProjectionTime;
   CNeuronBatchNormOCL     cNormalizeTime;
   CNeuronTransposeOCL     cTransposeOut;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronResNeXtResidual(void){};
                    ~CNeuronResNeXtResidual(void){};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint chanels_in, uint chanels_out,
                          uint units_in, uint units_out,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronResNeXtResidual;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual CLayerDescription* GetLayerInfo(void) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

En el desarrollo de este objeto, usamos enfoques similares a la construcción por módulos Bottleneck, pero adaptados a otras dimensiones del tensor original.

En la estructura de objetos presentada observamos varios objetos anidados cuya funcionalidad se describirá durante la implementación de los métodos de la nueva clase. Todos los objetos internos se declararán estáticamente, lo cual nos permitirá dejar el constructor y el destructor de la clase vacíos. La inicialización de todos los objetos, incluidos los heredados, se realiza en el módulo Init, en cuyos parámetros obtenemos un conjunto de constantes que nos permiten interpretar inequívocamente la arquitectura del objeto creado.

bool CNeuronResNeXtResidual::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                  uint chanels_in, uint chanels_out,
                                  uint units_in, uint units_out,
                                  ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, chanels_in, chanels_in, chanels_out,
                            units_out, 1, optimization_type, batch))
      return false;

En el cuerpo del método llamamos directamente al método homónimo de la clase padre, pasándole el conjunto de parámetros necesarios. De forma similar a los módulos bottleneck, utilizamos la capa de convolución como clase padre. Este también implementa la funcionalidad de proyección de datos en la nueva dimensión de característica.

Tras realizar con éxito las operaciones de la clase padre para inicializar los objetos e interfaces heredados, vamos a trabajar con los objetos recién declarados. Para que resulte más cómodo trabajar con los datos en la dimensionalidad del paso temporal, primero transponemos los datos de origen.

   int index=0;
   if(!cTransposeIn.Init(0, index, OpenCL, units_in, chanels_in, optimization, iBatch))
      return false;

A continuación, usando la capa convolucional, realizamos la proyección de secuencias unitarias individuales en la dimensión especificada.

   index++;
   if(!cProjectionTime.Init(0, index, OpenCL, units_in, units_in, units_out, chanels_in, 1, optimization, iBatch))
      return false;

Luego normalizamos los resultados obtenidos de forma similar al módulo bottleneck. Sin embargo, en este caso no se usa la función de activación. Al fin y al cabo, estamos construyendo un módulo de enlace residual y necesitamos transferir toda la información sin pérdidas.

   index++;
   if(!cNormalizeTime.Init(0, index, OpenCL, cProjectionTime.Neurons(), iBatch, optimization))
      return false;

A continuación, debemos ajustar el espacio de características. Para ello, realizamos una transposición inversa de los datos. Y la proyección se realiza directamente usando la clase padre.

   index++;
   if(!cTransposeOut.Init(0, index, OpenCL, chanels_in, units_out, optimization, iBatch))
      return false;
//---
   return true;
  }

Todo lo que debemos hacer es retornar el resultado lógico de las operaciones al programa que realiza la llamada y finalizar el método de inicialización del nuevo objeto.

A continuación procedemos a construir un algoritmo de pasada directa dentro del método feedForward.

bool CNeuronResNeXtResidual::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Projection Timeline
   if(!cTransposeIn.FeedForward(NeuronOCL))
      return false;

En los parámetros del método obtenemos el puntero al objeto que contiene los datos de origen. Luego pasamos este puntero al método homónimo de la capa interna de transposición de datos, que realiza la conversión de datos al formato de secuencia unitaria.

A continuación, tenemos que cambiar la dimensionalidad de las secuencias unitarias a un tamaño determinado. Para ello se usa una capa de convolución.

   if(!cProjectionTime.FeedForward(cTransposeIn.AsObject()))
      return false;

Después los datos obtenidos se normalizan.

   if(!cNormalizeTime.FeedForward(cProjectionTime.AsObject()))
      return false;

A continuación, realizamos una transposición inversa de los datos y los proyectamos en el espacio de características.

//--- Projection Chanels
   if(!cTransposeOut.FeedForward(cNormalizeTime.AsObject()))
      return false;
   return CNeuronConvOCL::feedForward(cTransposeOut.AsObject());
  }

La última proyección se realiza usando la clase padre. Luego retornamos el resultado lógico de las operaciones al programa que realiza la llamada y finalizamos el método de pasada directa.

Podemos ver fácilmente que el algoritmo del método de pasada directa tiene una estructura lineal. Esto provoca la linealidad de los flujos de distribución del gradiente de error durante las operaciones de pasada inversa. Por ello, le sugiero dejar los métodos de pasada inversa para el estudio individual, de forma similar al objeto CNeuronResNeXtBottleneck. Encontrará el código completo de los objetos anteriores y de todos sus módulos en el archivo adjunto.

El bloque ResNeXt


Arriba hemos creado objetos aparte que representan los dos flujos de información del marco ResNeXt. Ahora es el momento de combinar estos objetos en una única estructura que nos permitirá trabajar con los datos de forma más eficiente. Para ello, crearemos un objeto CNeuronResNeXtBlock, que servirá como bloque principal para el posterior procesamiento de la información. La estructura de este mecanismo se resume más abajo:

class CNeuronResNeXtBlock :  public CNeuronBaseOCL
  {
protected:
   uint                     iChanelsOut;
   CNeuronResNeXtBottleneck cBottleneck;
   CNeuronResNeXtResidual   cResidual;
   CBufferFloat             cBuffer;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronResNeXtBlock(void){};
                    ~CNeuronResNeXtBlock(void){};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint chanels_in, uint chanels_out, uint window,
                          uint step, uint units_count, uint group_size, uint groups,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronResNeXtBlock;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual CLayerDescription* GetLayerInfo(void) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

En la estructura presentada vemos objetos ya conocidos y un conjunto familiar de métodos virtuales que debemos redefinir.

Todos los objetos internos se declararán estáticamente, lo que nos permitirá dejar vacíos el constructor y el destructor de la clase, La inicialización de todos los objetos declarados y heredados se realiza como antes en el método Init. La estructura de los parámetros de este método se toma al completo del objeto CNeuronResNeXtBottleneck.

bool CNeuronResNeXtBlock::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                               uint chanels_in, uint chanels_out, uint window,
                               uint step, uint units_count, uint group_size, uint groups,
                               ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   int units_out = ((int)units_count - (int)window + (int)step - 1) / (int)step + 1;
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_out * chanels_out, optimization_type, batch))
      return false;

En el cuerpo del método, primero definimos la dimensionalidad de la secuencia a la salida del bloque y, después, inicializamos las interfaces básicas heredadas del objeto padre.

Tras realizar con éxito las operaciones del método de inicialización de la clase padre, guardamos los parámetros necesarios en nuestras variables de objeto.

   iChanelsOut = chanels_out;

E inicializamos los objetos internos de los flujos de información construidos anteriormente.

   int index = 0;
   if(!cBottleneck.Init(0, index, OpenCL, chanels_in, chanels_out, window, step, units_count,
                       group_size, groups, optimization, iBatch))
      return false;
   index++;
   if(!cResidual.Init(0, index, OpenCL, chanels_in, chanels_out, units_count, units_out, optimization, iBatch))
      return false;

En la salida del bloque se debe recibir la suma de los valores de los dos flujos de información. Por consiguiente, podemos propagar completamente el gradiente de error resultante a ambos flujos de datos de información. Como es habitual en estos casos, para evitar operaciones innecesarias de copiado de datos, sustituimos los punteros por los búferes de datos correspondientes.

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

Ahora todo lo que debemos hacer es devolver el resultado lógico de las operaciones al programa que realiza la llamada y finalizar el método.

A continuación, pasaremos a la construcción de los algoritmos de pasada inversa dentro del método feedForward. Aquí todo resulta bastante sencillo.

bool CNeuronResNeXtBlock::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cBottleneck.FeedForward(NeuronOCL))
      return false;
   if(!cResidual.FeedForward(NeuronOCL))
      return false;

En los parámetros del método, obtenemos el puntero al objeto de datos de origen, que pasamos directamente a los métodos homónimos de los objetos de los dos flujos de información. Acto seguido, resumimos y normalizamos los resultados obtenidos.

   if(!SumAndNormilize(cBottleneck.getOutput(), cResidual.getOutput(), Output, iChanelsOut, true, 0, 0, 0, 1))
      return false;
//--- result
   return true;
  }

Luego retornamos el resultado lógico de las operaciones al programa que realiza la llamada y finalizamos el método de pasada directa.

Aunque la estructura pueda parecer sencilla a primera vista, en realidad incluye dos flujos de información, lo cual añade cierta complejidad a la hora de realizar las operaciones de distribución del gradiente de error. El algoritmo responsable de este proceso se implementa en el método calcInputGradients.

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

En los parámetros del método, obtenemos el puntero al objeto de datos de origen utilizado en la pasada directa. Solo que en este caso deberemos pasarle el gradiente de error según la influencia de los datos de entrada en el resultado final del modelo. Solo podemos transferir los datos a un objeto válido. Por consiguiente, primero comprobamos la relevancia del puntero recibido antes de continuar con las operaciones.

Tras pasar con éxito el bloque de control, transmitimos el gradiente de error por uno de los flujos de información.

   if(!NeuronOCL.calcHiddenGradients(cBottleneck.AsObject()))
      return false;

A continuación, antes de transmitir el gradiente de error del segundo flujo de información, debemos guardar los datos recibidos. Sin embargo, no realizamos un copiado completo de los datos. En su lugar, usamos el mecanismo de sustitución de punteros por búferes de datos. Luego almacenamos el puntero al búfer de gradiente de error de datos de origen en una variable local.

   CBufferFloat *temp = NeuronOCL.getGradient();

A continuación, comprobamos que el búfer auxiliar coincida con la dimensionalidad del búfer de gradiente. Si es necesario, ajustamos el búfer auxiliar.

   if(cBuffer.GetOpenCL() != OpenCL ||
      cBuffer.Total() != temp.Total())
     {
      if(!cBuffer.BufferInitLike(temp))
         return false;
     }

Y pasamos su puntero al objeto de datos de origen.

   if(!NeuronOCL.SetGradient(GetPointer(cBuffer), false))
      return false;

Ahora podemos pasar con seguridad el gradiente de error por el segundo flujo de información sin miedo a perder datos.

   if(!NeuronOCL.calcHiddenGradients(cResidual.AsObject()))
      return false;

Después sumamos los valores de los dos flujos de información y retornamos los punteros a los búferes de datos a su estado inicial.

   if(!SumAndNormilize(temp, NeuronOCL.getGradient(), temp, 1, false, 0, 0, 0, 1))
      return false;
   if(!NeuronOCL.SetGradient(temp, false))
      return false;
//---
   return true;
  }

A continuación, devolvemos el resultado lógico de las operaciones al programa que realiza la llamada y finalizamos el método de distribución del gradiente de error.

Con esto concluye el análisis de los algoritmos de construcción de los métodos del objeto ResNeXt-block. Podrá leer el código completo del objeto especificado y todos sus métodos por sí mismo en el archivo adjunto.

Bien, ya hemos llegado al final del artículo, pero nuestro trabajo aún no ha terminado. Haremos una pausa y continuaremos el trabajo iniciado en el próximo artículo.



Conclusión

En este trabajo, nos hemos familiarizado con un framework de aprendizaje multitarea basado en la arquitectura ResNeXt propuesto para el procesamiento de datos financieros. Este framework permite extraer y procesar características de forma eficaz, optimizando las tareas de clasificación y regresión en entornos de alta dimensionalidad y series temporales.

En la parte práctica del artículo, hemos construido los principales elementos de la arquitectura ResNeXt. El siguiente artículo nos centraremos en la creación de un framework de aprendizaje multitarea y probaremos la eficacia de los enfoques implementados con datos históricos reales.

Enlaces


Programas usados en el artículo

#NombreTipoDescripción
1Research.mq5AsesorAsesor de recopilación de datos
2ResearchRealORL.mq5
Asesor
Asesor de recopilación de ejemplos con el método Real-ORL
3Study.mq5AsesorAsesor de entrenamiento de modelos
4Test.mq5AsesorAsesor para la prueba de modelos
5Trajectory.mqhBiblioteca de clasesEstructura de descripción del estado del sistema y la arquitectura del modelo
6NeuroNet.mqhBiblioteca de clasesBiblioteca de clases para crear una red neuronal
7NeuroNet.clBibliotecaBiblioteca de código del programa OpenCL

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

Archivos adjuntos |
MQL5.zip (2430.99 KB)
Edgar Akhmadeev
Edgar Akhmadeev | 16 ene 2026 en 21:25
Alain Verleyen #:
Según mi experiencia, los operadores que pueden compartir algo realmente útil nunca comparten nada.

Sí, saben (como yo desde 1998) que una estrategia que funciona rápidamente deja de funcionar después de su distribución.

Es por eso que los programadores de foros comparten soluciones individuales, mientras que una estrategia de trabajo (rentable) nunca se ha publicado. O vendida.

lynxntech
lynxntech | 16 ene 2026 en 21:29
Edgar Akhmadeev #:

Sí, saben (como yo desde 1998) que una estrategia que funciona rápidamente deja de funcionar una vez difundida.

Por eso los programadores de los foros comparten soluciones individuales, y nunca se ha publicado una estrategia (rentable) que funcione. O vendida.

y la necesidad de transferir fondos entre países ya no cuenta).

¿Cómo puede ser un sistema de este tipo?

Un robot de comercio siempre funcionará si usted compra en un retroceso, la pregunta es ¿dónde está el retroceso?

lynxntech
lynxntech | 16 ene 2026 en 21:47
He visto la traducción, definitivamente no soy traducible.
Edgar Akhmadeev
Edgar Akhmadeev | 16 ene 2026 en 22:21
lynxntech #:
He visto la traducción, definitivamente no soy traducible

Tengo que admitir que no era lo suficientemente inteligente para entender el original.

"¡He estado hablando solo toda la noche, y no me han entendido!" (Zhvanetsky

Vitaly Muzichenko
Vitaly Muzichenko | 17 ene 2026 en 00:40
Edgar Akhmadeev #:
Sí, saben (como yo desde 1998) que una estrategia que funciona rápidamente deja de funcionar en cuanto se difunde.

Esto se aplica a las bolsas con liquidez limitada, no se aplica a forex, hay suficiente liquidez allí para todo el mundo

P.D. Me acordé de Mikhail, tiene un sistema de cobertura en la Bolsa de Moscú, lo compartió y funciona, y debería funcionar en el futuro. Todo depende del capital personal, y no hay nada que hacer allí con 100 dólares.

Aquí, todo el mundo está buscando un sistema para un centenar de libras, y la rentabilidad del 10% por día. Por eso tales resultados de búsquedas.

Implementación del algoritmo criptográfico SHA-256 desde cero en MQL5 Implementación del algoritmo criptográfico SHA-256 desde cero en MQL5
La creación de integraciones de intercambio de criptomonedas sin DLL ha sido durante mucho tiempo un reto, pero esta solución proporciona un marco completo para la conectividad directa con el mercado.
Sistemas neurosimbólicos en trading algorítmico: Combinación de reglas simbólicas y redes neuronales Sistemas neurosimbólicos en trading algorítmico: Combinación de reglas simbólicas y redes neuronales
El artículo relata la experiencia del desarrollo de un sistema comercial híbrido que combine el análisis técnico clásico con las redes neuronales. El autor describe detalladamente la arquitectura del sistema, desde el análisis básico de patrones y la estructura de la red neuronal hasta los mecanismos de toma de decisiones comerciales, compartiendo código real y observaciones de carácter práctico.
Desarrollo de un asesor experto para el análisis de eventos de noticias basados en el calendario en MQL5 Desarrollo de un asesor experto para el análisis de eventos de noticias basados en el calendario en MQL5
La volatilidad tiende a alcanzar su punto máximo alrededor de eventos noticiosos de alto impacto, lo que crea oportunidades de ruptura significativas. En este artículo, describiremos el proceso de implementación de una estrategia de ruptura basada en el calendario. Cubriremos todo, desde la creación de una clase para interpretar y almacenar datos del calendario, el desarrollo de backtests realistas utilizando estos datos y, finalmente, la implementación del código de ejecución para operaciones en vivo.
La estrategia comercial de captura de liquidez La estrategia comercial de captura de liquidez
La estrategia de negociación basada en la captura de liquidez es un componente clave de Smart Money Concepts (SMC), que busca identificar y aprovechar las acciones de los actores institucionales en el mercado. Implica apuntar a áreas de alta liquidez, como zonas de soporte o resistencia, donde las órdenes grandes pueden desencadenar movimientos de precios antes de que el mercado reanude su tendencia. Este artículo explica en detalle el concepto de «liquidity grab» (captura de liquidez) y describe el proceso de desarrollo de la estrategia de negociación basada en la captura de liquidez en MQL5.