Русский Português
preview
Redes neuronales en el trading: Previsión probabilística de series temporales (Codificador)

Redes neuronales en el trading: Previsión probabilística de series temporales (Codificador)

MetaTrader 5Sistemas comerciales |
33 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

Continuamos nuestra introducción al framework K²VAE, una arquitectura progresiva diseñada específicamente para el modelado de series temporales en condiciones de alta incertidumbre. Este modelo se basa en la síntesis de tres ideas clave: la dinámica lineal en el espacio latente (mediante operadores de Koopman), el filtrado de errores adaptativo (utilizando KalmanNet) y el modelado probabilístico (basado en autoencoders variacionales). Este enfoque nos permite considerar simultáneamente tanto los patrones presentes en los datos como el grado de confianza en dichos patrones.

La principal ventaja del K²VAE es que no se limita a generar una predicción, sino que forma una distribución de probabilidad de los estados futuros del sistema. A diferencia de los modelos tradicionales, que se limitan a un único escenario probable, el resultado aquí es una gama de posibles desenlaces. Además, la amplitud de este rango depende del grado de confianza del modelo en el estado actual. Esto hace que el framework resulte particularmente útil en áreas donde es importante considerar el riesgo y la incertidumbre, como la previsión financiera, la logística o la gestión de sistemas técnicos.

Para comprender cómo se logra dicha flexibilidad y adaptabilidad, analicemos la arquitectura general del modelo. La arquitectura de K²VAE puede dividirse convencionalmente en tres componentes principales: el parcheo, el Codificador y el Decodificador tienen cada uno su propia función, pero están estrechamente relacionados entre sí.

  1. El proceso de parcheo prepara los datos de origen y los convierte en una representación latente.
  2. El Codificador es responsable de extraer el estado oculto Z de la serie temporal observada X. A diferencia de los VAE estándar, utiliza una estructura compleja que incluye:
    • KoopmanNet, un análogo entrenable del operador de Koopman que predice la evolución de características latentes como un sistema lineal;
    • Un módulo de atención que analiza las diferencias entre los valores reconstruidos y los reales, lo cual permite identificar los momentos en que el modelo diverge de la realidad;
    • KalmanNet, una implementación de red neuronal híbrida del filtro de Kalman que genera una matriz de covarianza de incertidumbre basada en señales de control atencional;
    • El VAE es un mecanismo que muestrea tokens futuros basándose en parámetros especificados por KalmanNet y KoopmanNet.
  3. El Decodificador transforma las variables latentes de nuevo en variables observadas, reconstruyendo los valores predichos de la serie temporal. Además, para respetar la naturaleza probabilística del modelo, el Decodificador también se implementa como una estructura de red neuronal entrenable con dos salidas: el valor medio y la varianza. Esto nos permite modelar completamente la distribución de P (Y|Z) y tener en cuenta la incertidumbre del pronóstico.

A continuación le presentamos la visualización del framework K²VAE realizada por el autor.

En el artículo anterior, nos centramos en los fundamentos: la implementación de la infraestructura básica, el soporte para la reutilización de matrices entrenables y el establecimiento de las bases para una arquitectura correcta y escalable. Asimismo, creamos las clases universales para generar parámetros, depuramos un mecanismo para actualizarlas mediante procedimientos estándar de propagación inversa de errores y describimos una estructura adecuada para la posterior expansión de componentes. De este modo, hemos sentado las bases sobre las que ahora podemos construir módulos más complejos.

El siguiente paso lógico será desarrollar un Codificador que realiza una función crítica, a saber, transformar una serie temporal original en la representación latente oculta correspondiente para el análisis lineal y el modelado probabilístico. Además, esto no se logra mediante un simple conjunto de capas, sino mediante una estructura cuidadosamente diseñada.



Discusión sobre los enfoques de construcción

Antes de comenzar con la implementación práctica del Codificador utilizando el lenguaje MQL5, echemos un vistazo detallado a su estructura arquitectónica y lógica de construcción. Esto nos ayudará no solo a orientar correctamente nuestro futuro trabajo, sino también a comprender cómo contribuye cada módulo al comportamiento general del modelo.

El Codificador utilizado en el framework K²VAE está construido sobre un principio modular e incluye cuatro componentes interconectados, cada uno de los cuales realiza una función estrictamente definida en el proceso de procesamiento de series temporales.

El primer elemento de la cadena de procesamiento es KoopmanNet, una interpretación de red neuronal del operador de Koopman, cuya tarea principal es aproximar la dinámica lineal en una serie temporal. Este recibe como entrada un estado oculto y devuelve una predicción del siguiente estado, suponiendo que el comportamiento del sistema puede representarse como un desplazamiento lineal en algún espacio latente. Sin embargo, dado que las series temporales reales rara vez siguen una trayectoria estrictamente lineal, la proyección en el espacio latente de KoopmanNet por sí sola no es suficiente para un modelado fiable. Para que el modelo no solo reprodujera la tendencia, sino que también evaluara su propia precisión, los autores del framework propusieron un mecanismo muy original.

KoopmanNet, además de predecir el siguiente estado, también aprende a reconstruir las transiciones que ya han sucedido; esencialmente, aprende a reconstruir retrospectivamente la trayectoria a partir de valores anteriores de la serie temporal. De este modo, el modelo no se limita a avanzar, sino que también mira hacia atrás, comprobando hasta qué punto sus parámetros actuales se ajustan a las observaciones pasadas. Este pensamiento bidireccional permite que el modelo calcule la diferencia entre la dinámica esperada y los estados reales del sistema.

Precisamente dicha desviación (la diferencia entre las transiciones calculadas por KoopmanNet y los valores históricos reales) sirve de base para evaluar la calidad de la aproximación lineal. De esta forma, el modelo adquiere la capacidad de autodiagnosticarse y puede adaptarse dinámicamente a los cambios o inestabilidades sin perder la conexión con la estructura lineal que lo sustenta.

En la implementación original del framework K²VAE, el módulo KoopmanNet consta de dos modelos pequeños totalmente conectados separados (MLP), cada uno de los cuales modela un aspecto diferente de las transiciones en el espacio latente: uno es responsable de las dependencias globales, y el otro de las locales. Este enfoque tiene mucho sentido desde una perspectiva teórica, pero en la práctica puede resultar demasiado rígido y limitante, sobre todo en el contexto de datos de mercado volátiles.

Le proponemos reforzar KoopmanNet utilizando un bloque de mezcla dispersa de expertos más general y flexible, CNeuronTimeMoESparseExperts. Este es un módulo del framework TimeMoE que implementa el principio de mezcla dispersa de expertos (MoE), un mecanismo en el que muchos submodelos especializados (expertos) operan en paralelo, mientras que el mecanismo de control selecciona el más adecuado según el contexto inicial.

El bloque de mezcla dispersa de expertos no es solo una implementación técnica; es una base lógica para separar los patrones locales y globales presentes en las series temporales financieras. En este bloque trabajan dos tipos de expertos. El primero es un grupo de expertos locales especializados, cada uno de los cuales es responsable de reconocer las transiciones a corto plazo y los patrones que dependen del contexto. Estos expertos analizan fundamentalmente el comportamiento del mercado en periodos de tiempo limitados, adaptándose a la volatilidad actual y a las microtendencias.

El segundo es un experto global. Este se integra en el marco de trabajo CNeuronTimeMoESparseExperts y actúa como un operador lineal de Koopman generalizado. Su propósito consiste en capturar la dinámica estable a largo plazo de una serie temporal, identificando patrones que persisten durante todo el período de entrenamiento. Esto proporciona al modelo una doble visión de los datos: un análisis local detallado y una perspectiva estratégica amplia.

Esta arquitectura permite a KoopmanNet no solo predecir el siguiente estado, sino también restablecer la trayectoria anterior, reconstruyendo la serie de transiciones que llevaron a la posición actual. La comparación de las transiciones reconstruidas con los valores reales permite al sistema evaluar la precisión de su propia aproximación.

Una vez que KoopmanNet completa la reconstrucción de la dinámica y predice el siguiente paso, entra en juego el módulo de atención, cuya tarea no solo consiste en comparar los valores reales y simulados, sino también en analizar directamente las desviaciones entre ellos. A diferencia de los mecanismos de atención cruzada tradicionales, que comparan dos flujos de datos, la atención cruzada se centra exclusivamente en el tensor de errores, identificando estructuras y patrones repetitivos en él.

Los autores del framework K²VAE proponen considerar estas desviaciones no como un subproducto, sino como una fuente de información en sí misma. La idea es que los errores en sí mismos contienen una señal: indican dónde y con qué eficacia KoopmanNet se adapta a la descripción del sistema, y qué características locales de la dinámica se quedan fuera del campo de visión del modelo lineal. La atención nos permite extraer de estos errores un tensor de señales de control, una especie de instrucción para ajustar las previsiones en la siguiente etapa.

Así, en lugar de simplemente comparar las predicciones con los hechos, el modelo aprende a interpretar sus debilidades de forma significativa. Y es precisamente este enfoque el que sustenta la alta adaptabilidad e inteligencia de K²VAE, lo que le permite operar eficazmente en un entorno cambiante e inestable.

En el marco de la implementación propuesta, podemos utilizar uno de los módulos Self-Attention ya probados, aplicados con éxito anteriormente para el análisis de series temporales. Esto no solo acelera significativamente el proceso de desarrollo, sino que también garantiza la compatibilidad con otros componentes del framework. Este módulo es capaz de identificar patrones internos en la estructura de los errores, capturando patrones recurrentes y dependencias locales entre desviaciones en distintos intervalos de tiempo.

Es importante señalar que en este contexto, la Self-Attention no se utiliza para procesar la secuencia de datos de entrada original, como se suele hacer en los transformadores clásicos, sino para analizar los errores de reconstrucción que se producen al reconstruir la dinámica en KoopmanNet. De hecho, trasladamos el enfoque de atención del modelo de los propios datos al resultado de su propio trabajo, lo que permite al sistema evaluarse a sí mismo desde fuera y extraer información útil de los errores.

Sin embargo, al implementar este enfoque, surge un matiz interesante. La profundidad de la historia analizada y el horizonte de planificación para el que se generan las señales de control pueden variar en tamaño. En la versión original del framework, este problema se resuelve mediante una proyección lineal, una transformación compacta que permite adaptar el vector de control al horizonte de planificación requerido.

En nuestro caso, la situación es algo más sencilla. Como el modelo pretende generar un único token siguiente, no necesitamos una secuencia de señales de control, sino solo un vector. Obviamente, podemos utilizar la misma proyección lineal, pero le proponemos adoptar un enfoque diferente y centrarnos en el error del último token en el contexto de toda la cadena de desviaciones precedente.

Este enfoque abre nuevas posibilidades: en lugar de considerar un error como una cantidad aislada, lo analizamos en relación con la historia acumulada de imprecisiones. Esto permite que el mecanismo de atención determine qué fragmentos particulares de la dinámica pasada han influido más en el error actual. El resultado es un vector de control que no solo reacciona a la imprecisión actual, sino que también refleja sus causas, codificadas en los estados anteriores del sistema.

Este método posee una serie de ventajas evidentes:

  • Atención basada en el objetivo: el modelo se centra en el error del último paso en lugar de distribuir los recursos a lo largo de toda la trayectoria;
  • Reducción de la carga computacional: no es necesario formar vectores largos para cada paso futuro;
  • Mejor interpretabilidad: la señal de control se vuelve significativa y adecuada para el análisis visual y el diagnóstico.

El resultado final es un mecanismo de control interno flexible integrado en el Codificador que permite al modelo adaptarse a sus propias debilidades y generar con mayor confianza una distribución para el siguiente paso a través de KalmanNet y VAE. Este enfoque hace que la arquitectura no solo sea más resistente, sino también mucho más transparente en su comportamiento, lo cual resulta especialmente importante cuando se trabaja en condiciones de incertidumbre del mercado.

El siguiente eslabón en la arquitectura del Codificador es KalmanNet, un módulo que actúa como un generador adaptativo de matrices de covarianza. Su tarea no consiste simplemente en transmitir información de forma pasiva, sino en evaluar activamente el grado de confianza en la previsión lineal obtenida a partir de KoopmanNet. La señal de control procedente del bloque de atención se convierte en una especie de indicador de la fiabilidad del modelo anterior. Si las desviaciones son pequeñas y persistentes, KalmanNet lo interpreta como una alta confianza y, en consecuencia, reduce la distribución de covarianza, lo cual sugiere que es muy probable que el comportamiento posterior de la serie temporal siga el escenario previsto. Pero si existe un desequilibrio significativo o cambios estructurales en los errores, el modelo, por el contrario, expande la matriz de covarianza, abriendo espacio para resultados más variables y probabilísticos.

De este modo, KalmanNet actúa eficazmente como un indicador de confianza, es decir, una especie de barómetro sensible a las turbulencias presentes en los datos. No solo transmite señales, sino que las interpreta, transformando vibraciones invisibles en una forma matemática adecuada para su posterior uso.

El bloque de muestreo variacional (VAE) completa el trabajo del Codificador. Es aquí donde se forma la representación latente final del estado futuro. En este caso, la selección no se realiza a ciegas; está sujeta a la lógica establecida en las etapas previas. Los valores medios se toman de KoopmanNet como base para la dinámica estimada, mientras que el grado de dispersión viene dado por la matriz de covarianza generada por KalmanNet. Como resultado, obtenemos no una previsión puntual, sino un modelo probabilístico que considera tanto la estructura lineal de la serie temporal como la naturaleza estocástica del mercado.

Este enfoque tiene ventajas estratégicas evidentes. Esto permite que el modelo se mantenga flexible ante la incertidumbre, no centrándose en un único escenario de desarrollo, sino adaptando su nivel de confianza en función del contexto. Esto hace que el framework resulte particularmente adecuado para su uso en entornos donde predecir estados futuros resulta menos importante que la capacidad de tomar decisiones dinámicas basadas en evaluaciones de riesgo y confianza.

Para transmitir el grado de incertidumbre y el nivel de riesgos potenciales, en nuestra implementación no nos limitamos a una sola previsión. En cambio, en el siguiente paso, muestrearemos un número determinado de escenarios posibles para el desarrollo de la serie temporal. Cada uno de estos escenarios es una trayectoria separada generada a partir de la distribución común formada por KoopmanNet y KalmanNet. Este enfoque nos permite no solo obtener una estimación promedio, sino también abarcar una amplia gama de resultados probables, revelando así la fiabilidad con la que el modelo predice el futuro.

En conjunto, la interacción de KoopmanNet, el bloque de atención, KalmanNet y VAE forma una estructura compleja pero sorprendentemente lógica, donde cada componente desempeña un papel en la construcción de una representación fiable y adaptativa del futuro.



Estructura del objeto

Tras un análisis detallado de los principios arquitectónicos y la lógica de interacción de los componentes dentro del Codificador K²VAE, es hora de analizar su implementación práctica. Para garantizar la modularidad, la flexibilidad y la legibilidad del código, hemos combinado todos los elementos clave en un único objeto de clase CNeuronK2VAEEncoder, que hereda su funcionalidad principal de CNeuronBaseOCL.

Este objeto acumula todo lo necesario para el ciclo completo de trabajo del Codificador, desde el análisis de la dinámica de series temporales mediante KoopmanNet, hasta la construcción de la estructura de covarianza de estados futuros en KalmanNet y el muestreo de pronósticos probabilísticos a través del mecanismo VAE. El objeto contiene componentes especializados de la red Koopman, un bloque de atención y un filtro de Kalman, así como capas técnicas para transformaciones, operaciones lineales y cálculos matriciales.

A continuación le mostramos la estructura de la clase CNeuronK2VAEEncoder, donde cada componente implementa una función específica dentro del proceso general de codificación de una secuencia observada. Esta implementación permite el análisis y la depuración paso a paso en cualquier nivel de profundidad del modelo, y ofrece una escalabilidad avanzada al trabajar con diferentes configuraciones arquitectónicas y profundidades de análisis.

class CNeuronK2VAEEncoder   :  public CNeuronBaseOCL
  {
protected:
   //--- Koopman
   CNeuronTimeMoESparseExperts   cKoopman;
   CNeuronBaseOCL                cKoopmanPred;
   CNeuronBaseOCL                cKoopmanRest;
   //---
   CNeuronTimeMoEAttention       cAuxiliaryNet;
   //--- Kalman Filter
   CParams              cB;      // Control input matrix B
   CParams              cF;      // State transition matrix F
   CParams              cH;      // Observation matrix H
   CParams              cQ;      // Learnable covariance matrices Q
   CParams              cR;      // Learnable covariance matrices R
   CNeuronBaseOCL       cP;      // Covariance matrices P
   //---
   CNeuronTransposeOCL  cFT;
   CNeuronTransposeOCL  cHT;
   CNeuronTransposeOCL  cQT;
   CNeuronTransposeOCL  cRT;
   CNeuronTransposeOCL  cPT;
   //---
   CNeuronBaseOCL       cQ_QT;
   CNeuronBaseOCL       cR_RT;
   CNeuronBaseOCL       cXPred;
   CNeuronBaseOCL       cF_P;
   CNeuronBaseOCL       cPPred;
   //---
   CNeuronBaseOCL       cP_HT;
   CNeuronBaseOCL       cH_P_HT;
   matrix<double>       mS, mSGrad;
   CNeuronBaseOCL       cSInv;
   CNeuronBaseOCL       cK;
   CNeuronTransposeOCL  cKT;
   CNeuronBaseOCL       cYPred;
   CNeuronBaseOCL       cDeltY;
   CNeuronBaseOCL       cX;
   CNeuronBaseOCL       cK_H;
   CNeuronBaseOCL       cIdifK_H;
   CNeuronTransposeOCL  cIdifK_HT;
   matrix<double>       mP;
   matrix<double>       mPGrad;
   matrix<double>       mNoise;
   matrix<double>       mGrad;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronK2VAEEncoder(void) {};
                    ~CNeuronK2VAEEncoder(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_cross,
                          uint heads, uint layers, uint scenarios,
                          uint experts, uint experts_dimension, uint topK,
                          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)        const                      {  return defNeuronK2VAEEncoder; }
   virtual void      TrainMode(bool flag) override;
   virtual void      SetOpenCL(COpenCLMy *obj);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
  }; 

En la estructura presentada de la nueva clase, se debe prestar especial atención a los componentes clave que reflejan la lógica de toda la arquitectura del Codificador. Como KoopmanNet se utiliza el potente módulo CNeuronTimeMoESparseExperts, que combina la flexibilidad local de una mezcla dispersa de expertos con la robustez de un predictor global. Esta solución permite modelar de forma eficiente tanto la dinámica a corto plazo como la persistente en series temporales.

El módulo de atención es CNeuronTimeMoEAttention, que se encarga de analizar la estructura de los errores de reconstrucción. Este identifica patrones en las desviaciones, generando características de control para evaluar la confianza en las predicciones de KoopmanNet.

El bloque del filtro de Kalman ocupa un lugar especial en la implementación. Aquí vemos las matrices de entrenamiento B, F, H, Q y R, cada una representada como una instancia aparte de la clase CParams. Este enfoque permite no solo la gestión centralizada de los parámetros del filtro, sino también su completa adaptación durante el proceso de aprendizaje.

Además de estos componentes, la estructura de clases también contiene muchos objetos auxiliares que garantizan la correcta implementación de la lógica paso a paso de KalmanNet y permiten una gestión eficiente de todas las operaciones tensoriales dentro del Codificador. La funcionalidad de los objetos auxiliares se analizará con más detalle a medida que implementemos los métodos de la clase.

La organización interna de la clase CNeuronK2VAEEncoder presupone una declaración estática de todos los componentes usados. Esta solución nos permite simplificar el ciclo de vida de los objetos: no necesitamos realizar la asignación dinámica de memoria ni la limpieza manualmente. Por consiguiente, el constructor y el destructor de una clase pueden permanecer vacíos, sin sobrecargar el código con lógica innecesaria.

La configuración de toda la arquitectura, incluyendo la configuración de KoopmanNet, los parámetros de atención y el filtro de Kalman, está completamente contenida en el método Init. Precisamente aquí el objeto adquiere una forma específica: se configuran los parámetros de atención, se establecen los tamaños de las características de control, el número de expertos, la profundidad de la ventana de entrada y otras características críticas del modelo.

bool CNeuronK2VAEEncoder::Init(uint numOutputs, uint myIndex, COpenCLMy * open_cl,
                               uint window, uint window_key, uint units_cross,
                               uint heads, uint layers, uint scenarios,
                               uint experts, uint experts_dimension, uint topK,
                               ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * scenarios, optimization_type, batch))
      return false;

En la primera etapa, el control se transfiere a la función homónima de la clase básica. Esto permite usar mecanismos ya implementados para la validación básica de los parámetros de entrada, así como inicializar interfaces heredadas y componentes comunes. Este enfoque ofrece un estándar de inicialización único para todas las capas neuronales, lo que facilita el mantenimiento y la ampliación de la arquitectura.

La cuestión relacionada con la organización de la memoria merece especial atención. Como en nuestra implementación el codificador K²VAE no devuelve un único vector, sino un conjunto de posibles escenarios futuros generados mediante muestreo, el búfer de resultados debe tener un tamaño adecuado.

A continuación, procedemos a inicializar el bloque KoopmanNet, que en nuestra implementación está representado por el módulo CNeuronTimeMoESparseExperts. El módulo de mezcla dispersa de expertos lo inicializamos especificando el número de modelos y sus dimensiones. Este componente se encarga de extraer patrones tanto locales como globales en las series temporales, simulando el trabajo de los operadores de Koopman.

//--- Koopman
   int index = 0;
   if(!cKoopman.Init(0, index, OpenCL, window, 2 * window, units_cross, 1, experts, topK, optimization, iBatch))
      return false;
   index++;
   if(!cKoopmanPred.Init(0, index, OpenCL, window, optimization, iBatch))
      return false;
   index++;
   if(!cKoopmanRest.Init(0, index, OpenCL, window * (units_cross - 1), optimization, iBatch))
      return false;

Después de esto, inicializamos dos objetos auxiliares: cKoopmanPred y cKoopmanRest. El primero se utiliza para almacenar los valores de pronóstico calculados por KoopmanNet, mientras que el segundo se usa para reconstruir los estados ya observados de la serie temporal.

El siguiente paso es inicializar el módulo de atención, que en nuestra implementación está representado por el objeto cAuxiliaryNet.

index++;
if(!cAuxiliaryNet.Init(0, index, OpenCL, window, window_key, 1, window, units_cross - 1,
                                                   heads, layers, optimization, iBatch))
   return false;

Sin embargo, la parte clave de la arquitectura interna de la nueva clase se concentra en la organización de la operación del filtro de Kalman; precisamente aquí se ubica la mayor parte de los cálculos y el mayor volumen de lógica implementada en el método Init.

En la primera etapa, inicializamos secuencialmente las matrices cuadradas de los parámetros entrenables del filtro: estas son las matrices de transiciones de estado F, las acciones de control B, las observaciones H y las covarianzas de los errores del modelo Q y las observaciones R.

//--- Kalman Filter
   index++;
   if(!cB.Init(0, index, OpenCL, window * window, optimization, iBatch) ||
      !cB.Identity(window, window))
      return false;
   index++;
   if(!cF.Init(0, index, OpenCL, window * window, optimization, iBatch) ||
      !cF.Identity(window, window))
      return false;
   index++;
   if(!cH.Init(0, index, OpenCL, window * window, optimization, iBatch) ||
      !cH.Identity(window, window))
      return false;
   index++;
   if(!cQ.Init(0, index, OpenCL, window * window, optimization, iBatch) ||
      !cQ.Identity(window, window))
      return false;
   index++;
   if(!cR.Init(0, index, OpenCL, window * window, optimization, iBatch) ||
      !cR.Identity(window, window))
      return false;
   index++;
   if(!cP.Init(0, index, OpenCL, window * window, optimization, iBatch) ||
      !cP.getOutput().Fill(matrix<float>::Identity(window, window)))
      return false;

Durante el proceso de inicialización de cada una de estas matrices, se especifican sus dimensionalidades. Luego, para garantizar la estabilidad numérica del filtro de Kalman en las iteraciones iniciales del entrenamiento, todas estas matrices se rellenan con valores diagonales. Esta inicialización nos permite asegurar la positividad definida de las matrices de covarianza y evitar la inestabilidad en los cálculos asociados con la inversión de la matriz S durante la corrección de los pronósticos.

El método de inicialización diagonal no solo estabiliza los pasos iniciales del entrenamiento, sino que también garantiza una sensibilidad uniforme del modelo respecto a los diferentes componentes del estado latente, lo cual resulta fundamental al trabajar con series temporales en las que las tendencias dominantes pueden enmascarar señales débiles pero significativas.

El siguiente paso consiste en preparar los objetos responsables de transponer las matrices correspondientes. Este es un punto importante y, a primera vista, técnico, pero se relaciona directamente con el correcto funcionamiento de todo el algoritmo. La cuestión es que, durante los cálculos del filtro de Kalman, tenemos que referirnos repetidamente no solo a las propias matrices F, H, Q, R y P en sí mismas, sino también a sus versiones transpuestas. Para garantizar esto, preasignamos e inicializamos los objetos de transposición correspondientes.

index++;
if(!cFT.Init(0, index, OpenCL, window, window, optimization, iBatch))
   return false;
index++;
if(!cHT.Init(0, index, OpenCL, window, window, optimization, iBatch))
   return false;
index++;
if(!cQT.Init(0, index, OpenCL, window, window, optimization, iBatch))
   return false;
index++;
if(!cRT.Init(0, index, OpenCL, window, window, optimization, iBatch))
   return false;
index++;
if(!cPT.Init(0, index, OpenCL, window, window, optimization, iBatch))
   return false;

De esta forma, se implementa una especie de almacenamiento temporal de formas transpuestas, lo cual acelera significativamente los cálculos y simplifica la estructura del código en la parte principal del algoritmo de filtrado. Además, esto aumenta la modularidad de la arquitectura, ya que permite que cada elemento del filtro se gestione de forma independiente, sin interrumpir la lógica de ejecución general.

Conviene prestar especial atención a las matrices de covarianza del ruido: Q (covarianza del proceso) y R (covarianza de la observación). Para que el algoritmo del filtro de Kalman funcione correctamente, estas matrices deben tener dos propiedades clave: deben ser diagonalmente simétricas y definidas positivas. La infracción de estas condiciones puede provocar inestabilidad en el filtrado, la aparición de valores imaginarios durante la inversión de la matriz o una degradación completa de la estimación del estado.

Para garantizar que estas propiedades se mantengan durante el entrenamiento, empleamos una técnica probada y ampliamente usada: la representación de las matrices Q y R como el producto de las propias matrices y su copia transpuesta. Esta representación hace automáticamente que las matrices resultantes sean simétricas y definidas positivas (a menos que la matriz original sea singular). Esto elimina la necesidad de controlar manualmente los valores en la diagonal o introducir la regularización.

index++;
if(!cQ_QT.Init(0, index, OpenCL, window * window, optimization, iBatch))
   return false;
index++;
if(!cR_RT.Init(0, index, OpenCL, window * window, optimization, iBatch))
   return false;

El siguiente paso en la configuración del Codificador es la inicialización de los objetos destinados a almacenar los resultados de los cálculos intermedios. Estos objetos desempeñan un papel fundamental en la implementación paso a paso del algoritmo del filtro de Kalman, donde cada cálculo se basa en los resultados de las operaciones anteriores. Además, también usaremos estos valores en la pasada inversa para distribuir correctamente el gradiente de error.

Comenzamos inicializando el objeto cXPred, que almacena el estado previsto del sistema antes de aplicar el paso correctivo.

index++;
if(!cXPred.Init(0, index, OpenCL, window, optimization, iBatch))
   return false;
index++;
if(!cF_P.Init(0, index, OpenCL, window * window, optimization, iBatch))
   return false;
index++;
if(!cPPred.Init(0, index, OpenCL, window * window, optimization, iBatch))
   return false;

A continuación, inicializamos los objetos cF_P y cPPred, que almacenan, respectivamente, el producto de la matriz de transición por la matriz de covarianza y la covarianza predicha. Estos datos son necesarios para que las operaciones posteriores permitan evaluar la fiabilidad del pronóstico realizado.

Luego, se configuran secuencialmente los bloques cP_HT y cH_P_HT, que son responsables de calcular los valores intermedios al calcular la matriz S, que representa la covarianza del error de pronóstico de las observaciones.

   index++;
   if(!cP_HT.Init(0, index, OpenCL, window * window, optimization, iBatch))
      return false;
   index++;
   if(!cH_P_HT.Init(0, index, OpenCL, window * window, optimization, iBatch))
      return false;
//---
   mS = mSGrad = matrix<double>::Zeros(window, window);

Paralelamente, se crean e inicializan las matrices mS y mSGrad, que se usarán para almacenar los valores de covarianza y los gradientes correspondientes durante el entrenamiento.

El objeto cSInv se encarga de almacenar la matriz inversa de S necesaria para calcular la matriz de Kalman cK, que también viene acompañada de su propia versión transpuesta cKT.

   index++;
   if(!cSInv.Init(0, index, OpenCL, window * window, optimization, iBatch))
      return false;
   index++;
   if(!cK.Init(0, index, OpenCL, window * window, optimization, iBatch))
      return false;
   index++;
   if(!cKT.Init(0, index, OpenCL, window, window, optimization, iBatch))
      return false;
//---
   index++;
   if(!cYPred.Init(0, index, OpenCL, window, optimization, iBatch))
      return false;
//---
   index++;
   if(!cDeltY.Init(0, index, OpenCL, window, optimization, iBatch))
      return false;

A continuación, se procede a la inicialización del bloque cYPred, que predice los valores observados, y de cDeltY, que calcula la discrepancia entre la predicción y la observación real.

Asimismo, se presta especial atención al objeto cX, que almacena el estado final corregido tras la aplicación del filtro de Kalman.

   index++;
   if(!cX.Init(0, index, OpenCL, window, optimization, iBatch))
      return false;
//---
   index++;
   if(!cK_H.Init(0, index, OpenCL, window * window, optimization, iBatch))
      return false;
//---
   index++;
   if(!cIdifK_H.Init(0, index, OpenCL, window * window, optimization, iBatch))
      return false;
//---
   index++;
   if(!cIdifK_HT.Init(0, index, OpenCL, window, window, optimization, iBatch))
      return false;

Luego se utilizan cK_H, cIdifK_H y cIdifK_HT, que se emplean para registrar los resultados de las transformaciones matemáticas necesarias para la correcta transmisión inversa de errores y la actualización de las estimaciones de covarianza.

Finalmente, inicializamos las matrices mP y mPGrad, así como los arrays auxiliares mNoise y mGrad, que se utilizarán para generar ruido en la fase de muestreo y en el proceso de cálculo de gradientes durante el entrenamiento del modelo.

   mP = mPGrad = mS;
   mNoise = mGrad = matrix<double>::Zeros(scenarios, window);
//---
   return true;
  }

De este modo, en esta etapa se crea una infraestructura informática completa que garantiza el funcionamiento estable y correcto del filtro de Kalman como parte del Codificador K²VAE. Una vez que todos los componentes internos se han inicializado correctamente, el método de inicialización finaliza y retorna el resultado lógico de las operaciones realizadas al programa que ha realizado la llamada.



Organización de la pasada directa

Al pasar de la etapa de inicialización de los objetos de almacenamiento a la descripción del método de pasada directa feedForward, descendemos a las profundidades de la lógica computacional del Codificador K²VAE. Aquí es donde todos los componentes previamente preparados se combinan en un solo sistema. La estructura completa, como un mecanismo bien engrasado, comienza su funcionamiento, transformando las observaciones iniciales en representaciones internas.

La pasada directa comienza con el bloque de Koopman, un componente clave del modelo que aprende la dinámica lineal del sistema durante el entrenamiento. En la etapa de inferencia, bajo condiciones de pasada directa, este módulo genera una serie completa de reconstrucciones y un estado predictivo basado en la secuencia analizada y la estructura lineal de cambios aprendida previamente. De este modo, su resultado incluye dos componentes importantes: en primer lugar, una previsión del estado futuro basada únicamente en las últimas observaciones y en los patrones lineales aprendidos, y en segundo lugar, una reconstrucción de toda la secuencia de estados anteriores, hasta el nivel de análisis de los datos históricos.

bool CNeuronK2VAEEncoder::feedForward(CNeuronBaseOCL * NeuronOCL)
  {
//--- Koopman
   if(!cKoopman.FeedForward(NeuronOCL))
      return false;
//--- Pred / Rest
   if(!DeConcat(cKoopmanPred.getOutput(), cKoopmanRest.getPrevOutput(), cKoopman.getOutput(),
                cKoopman.GetWindow(), cKoopman.Neurons() - cKoopman.GetWindow(), 1))
      return false;
   if(!Different(NeuronOCL.getOutput(), cKoopmanRest.getPrevOutput(),
                 cKoopmanRest.getOutput(), cKoopman.GetWindow()))
      return false;

Esta división resulta particularmente importante: la predicción se utiliza en el bloque del filtro de Kalman, mientras que la secuencia residual (la diferencia entre los datos reales y su reconstrucción) se transmite al módulo de atención, que procesa aquellos aspectos de la dinámica que no se ajustan al modelo lineal.

//--- Rest Attention
   if(!cAuxiliaryNet.FeedForward(cKoopmanRest.AsObject()))
      return false;

Si el modelo se encuentra en modo de entrenamiento, se activan todos los componentes entrenables del filtro de Kalman. Estas son las matrices de transición de estado, de acción de control y observación, así como las matrices de covarianza del modelo y los errores de medición.

//--- Kalman Filter
   if(bTrain)
     {
      if(!cB.FeedForward())
         return false;
      if(!cF.FeedForward())
         return false;
      if(!cH.FeedForward())
         return false;
      if(!cQ.FeedForward())
         return false;
      if(!cR.FeedForward())
         return false;
      if(!cFT.FeedForward(cF.AsObject()))
         return false;
      if(!cHT.FeedForward(cH.AsObject()))
         return false;
      if(!cQT.FeedForward(cQ.AsObject()))
         return false;
      if(!cRT.FeedForward(cR.AsObject()))
         return false;

Como el correcto funcionamiento del filtro requiere que las matrices Q y R sean definidas positivas, estas se preestabilizan multiplicándolas por sus propias copias transpuestas.

 if(!MatMul(cQ.getOutput(), cQT.getOutput(), cQ_QT.getOutput(),
            cQT.GetCount(), cQT.GetWindow(), cQT.GetCount(), 1, true))
    return false;
 if(!MatMul(cR.getOutput(), cRT.getOutput(), cR_RT.getOutput(),
            cRT.GetCount(), cRT.GetWindow(), cRT.GetCount(), 1, true))
    return false;
}

Una vez preparados todos los parámetros, comienza la fase de previsión. En primer lugar, se calcula una previsión del estado para el siguiente paso combinando dos flujos de información: los datos de origen y el módulo de atención. Estos vectores se proyectan usando las matrices de los parámetros entrenables F y B, y luego se suman. Como resultado, se forma un estado intermedio XPred.

//--- Prediction step
   if(!MatMul(NeuronOCL.getOutput(), cFT.getOutput(), cXPred.getGradient(),
              1, cFT.GetWindow(), cFT.GetCount(), 1, true))
      return false;
   if(!MatMul(cAuxiliaryNet.getOutput(), cB.getOutput(), cXPred.getPrevOutput(),
              1, cFT.GetWindow(), cFT.GetCount(), 1, true))
      return false;
   if(!SumAndNormilize(cXPred.getGradient(), cXPred.getPrevOutput(), cXPred.getOutput(),
                       cFT.GetCount(), false, 0, 0, 0, 1))
      return false;

A continuación, se calcula la covarianza del error del estado previsto. Esto se hace multiplicando sucesivamente la matriz de transición y la matriz de covarianza actual, y luego añadiendo la matriz de ruido del modelo Q, creando así la covarianza predictiva P_pred.

if(!MatMul(cF.getOutput(), cP.getOutput(), cF_P.getOutput(),
           cFT.GetCount(), cFT.GetWindow(), cP.Neurons() / cFT.GetWindow(), 1, true))
   return false;
if(!MatMul(cF_P.getOutput(), cFT.getOutput(), cPPred.getOutput(),
           cFT.GetCount(), cFT.GetWindow(), cFT.GetCount(), 1, true))
   return false;
if(!SumAndNormilize(cPPred.getOutput(), cQ_QT.getOutput(), cPPred.getOutput(),
                    cHT.GetWindow(), false, 0, 0, 0, 1))
   return false;

En esta etapa, comienza el ajuste de la previsión; esto es lo que hace que el filtro de Kalman sea tan valioso. En primer lugar, se calcula la covarianza del error de medición y se forma una matriz S que representa la suma de la varianza observada y la predicha.

//--- Update step
   if(!MatMul(cPPred.getOutput(), cHT.getOutput(), cP_HT.getOutput(),
              cPPred.Neurons() / cHT.GetWindow(), cHT.GetWindow(), cHT.GetCount(), 1, true))
      return false;
   if(!MatMul(cH.getOutput(), cP_HT.getOutput(), cH_P_HT.getOutput(),
              cHT.GetCount(), cHT.GetWindow(), cHT.GetCount(), 1, true))
      return false;
   if(!SumAndNormilize(cH_P_HT.getOutput(), cR_RT.getOutput(), cR_RT.getPrevOutput(),
                       cRT.GetWindow(), false, 0, 0, 0, 1))
      return false;

Para calcular la matriz de Kalman K, que actúa como coeficiente de ponderación en la corrección de estado, se usa la matriz inversa S. No hemos desarrollado desde cero un algoritmo para encontrar la matriz inversa. En su lugar, hemos usado la funcionalidad existente de las operaciones matriciales de MQL5. Si es necesario, la matriz se estabiliza, diagonaliza y reestructura para evitar la degeneración.

if(cR_RT.getPrevOutput().GetData(mSGrad) <= 0)
   return false;
mS = mSGrad.Inv();
if(mS.Rows() == 0)
  {
   mSGrad = mSGrad + mSGrad.Transpose();
   vector<double> eigvals;
   matrix<double> eigvecs;
   if(!mSGrad.Eig(eigvecs, eigvals))
      return false;
   if(eigvals.Size() > 0)
     {
      if(!eigvals.Clip(1e-6, DBL_MAX))
         return false;
      mSGrad = matrix<double>::Zeros(eigvals.Size(), eigvals.Size());
      mSGrad.Diag(eigvals);
      mSGrad = eigvecs.MatMul(mSGrad.MatMul(eigvecs.Transpose()));
      mSGrad = mSGrad + mSGrad.Transpose();
      mS = mSGrad.Inv();
     }
   if(mS.Rows() == 0)
     {
      mSGrad.Identity();
      mS = mSGrad;
     }
  }
cSInv.getOutput().Fill(mS);
if(!MatMul(cP_HT.getOutput(), cSInv.getOutput(), cK.getOutput(),
           cHT.GetCount(), (int)mS.Rows(), (int)mS.Cols(), 1, true))
   return false;

En la etapa de actualización de la medición, el modelo ajusta el estado predicho previamente no según las observaciones reales, sino en una previsión alternativa obtenida de KoopmanNet. Primero, el valor esperado de la observación se calcula multiplicando el estado predicho XPred por la matriz de observación transpuesta H. Este valor (YPred) refleja cuál debería ser la observación si el modelo no hubiera cometido un error en su pronóstico.

//--- Measurement update
   if(!MatMul(cXPred.getOutput(), cHT.getOutput(), cYPred.getOutput(),
              1, cHT.GetWindow(), cHT.GetCount(), 1, true))
      return false;
   if(!Different(cKoopmanPred.getOutput(), cYPred.getOutput(), cDeltY.getOutput(),
                 1, 0, 0, 0, 1))
      return false;
   if(!MatMul(cDeltY.getOutput(), cK.getOutput(), cX.getPrevOutput(), 1,
              cDeltY.Neurons(), cX.Neurons(), 1, true))
      return false;
   if(!SumAndNormilize(cX.getPrevOutput(), cXPred.getOutput(), cX.getOutput(), 1, false, 0, 0, 0, 1))
      return false;

La diferencia entre las predicciones de dos modelos se interpreta como un error. Al multiplicar este error por la matriz de Kalman K, obtenemos un vector de corrección, que se agrega al pronóstico original XPred, formando el estado corregido X.

Paralelamente, la matriz de covarianza del estado P se actualiza en la forma de Joseph estabilizada, lo que evita la acumulación de errores numéricos y preserva la simetría.

//--- Joseph stabilized form for P
   if(!MatMul(cK.getOutput(), cH.getOutput(), cK_H.getOutput(), cKT.GetCount(),
              cKT.GetWindow(), cHT.GetWindow(), 1, true))
      return false;
   if(!IdentDifferent(cK_H.getOutput(), cIdifK_H.getOutput(), cHT.GetWindow(), 0, 0, 1))
      return false;
   if(!cIdifK_HT.FeedForward(cIdifK_H.AsObject()))
      return false;
   if(!cKT.FeedForward(cK.AsObject()))
      return false;
   if(!MatMul(cIdifK_H.getOutput(), cPPred.getOutput(), cIdifK_H.getPrevOutput(),
              cIdifK_HT.GetCount(), cIdifK_HT.GetWindow(), cIdifK_HT.GetWindow(), 1, true))
      return false;
   if(!MatMul(cIdifK_H.getPrevOutput(), cIdifK_HT.getOutput(), cP.getPrevOutput(),
              cIdifK_HT.GetCount(), cIdifK_HT.GetWindow(), cIdifK_HT.GetCount(), 1, true))
      return false;
   if(!MatMul(cK.getOutput(), cR_RT.getOutput(), cK.getPrevOutput(),
              cKT.GetCount(), cRT.GetCount(), cRT.GetCount(), 1, true))
      return false;
   if(!MatMul(cK.getPrevOutput(), cKT.getOutput(), cKT.getPrevOutput(),
              cKT.GetCount(), cKT.GetWindow(), cKT.GetCount(), 1, true))
      return false;
   if(!SumAndNormilize(cP.getPrevOutput(), cKT.getPrevOutput(), cP.getOutput(), cPT.GetWindow(), false, 0, 0, 0, 1))
      return false;
   if(!cPT.FeedForward(cP.AsObject()))
      return false;
   if(!SumAndNormilize(cP.getOutput(), cPT.getOutput(), cP.getOutput(), 1, false, 0, 0, 0, 0.5f))
      return false;

Una vez finalizados todos los cálculos, se ejecuta el paso final: la generación del vector de salida. Para ello, se usa el muestreo basado en la matriz de covarianza inversa obtenida P⁻¹. El ruido aleatorio se escala por P, y el resultado se agrega al vector de estado X, formando la representación final.

   if(!SumAndNormilize(cX.getOutput(), cAuxiliaryNet.getOutput(), cX.getOutput(), 1, false, 0, 0, 0, 1))
      return false;
//--- Sample Output
   if(!cP.getOutput().GetData(mPGrad))
      return false;
   if(mPGrad.HasNan() > 0)
     {
      mPGrad.Identity();
      if(!cP.getOutput().Fill(mPGrad))
         return false;
     }
   mP = mPGrad.Inv();
   if(mP.Rows() == 0)
     {
      mPGrad = mPGrad + mPGrad.Transpose();
      vector<double> eigvals;
      matrix<double> eigvecs;
      if(!mPGrad.Eig(eigvecs, eigvals))
         return false;
      if(eigvals.Size() > 0)
        {
         if(!eigvals.Clip(1e-6, DBL_MAX))
            return false;
         mPGrad = matrix<double>::Zeros(eigvals.Size(), eigvals.Size());
         mPGrad.Diag(eigvals);
         mPGrad = eigvecs.MatMul(mPGrad.MatMul(eigvecs.Transpose()));
         mPGrad = mPGrad + mPGrad.Transpose();
         mP = mPGrad.Inv();
        }
      if(mP.Rows() == 0)
        {
         mPGrad.Identity();
         if(!cP.getOutput().Fill(mPGrad))
            return false;
         mP = mPGrad.Inv();
        }
     }
   mNoise.Random(-1, 1);
   matrix<double> temp = mNoise.MatMul(mP);
   if(!PrevOutput.Fill(temp))
      return false;
   if(!SumVecMatrix(cX.getOutput(), PrevOutput, Output, (int)mNoise.Cols(), 0, 0, 0, 1))
      return false;
//---
   return true;
  }

Cabe destacar que la etapa final de generación del tensor resultante no se limita a la construcción de un único escenario. En cambio, el modelo genera todo un conjunto de trayectorias posibles, cada una de las cuales supone una realización de una distribución multivariada descrita por la matriz de covarianza P. Esto no es solo un ingenioso truco matemático, sino un reflejo de la confianza del modelo en su propia predicción.

Precisamente este enfoque hace que el modelo sea particularmente valioso en condiciones de inestabilidad del mercado o ausencia de información fiable: puede no solo predecir un posible desarrollo futuro, sino también trazar toda una gama de trayectorias posibles, respaldadas por el entrenamiento. Esto convierte la salida de feedForward en una nube de decisiones probabilísticas. Y cada una de ellas refleja distintos aspectos del posible desarrollo de los acontecimientos.



Características de la distribución del gradiente de error

Una vez que el modelo ha generado un espectro de trayectorias posibles, finaliza la fase de pasada directa y comienza una etapa igualmente importante: la propagación del gradiente de error en la dirección opuesta. Lo implementamos en el método calcInputGradients, cuya tarea principal consiste en transferir los gradientes desde el nivel de salida del modelo a los datos de origen, propagando correctamente el error a través de todos los componentes conectados.

El algoritmo comienza con la distribución del gradiente entre los valores medios predichos del modelo lineal y la matriz de covarianza P.

bool CNeuronK2VAEEncoder::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//--- From Output
   if(!SumVecMatrixGrad(cX.getGradient(), PrevOutput, Gradient, (int)mNoise.Cols(), 0, 0, 0, 1))
      return false;
   if(!PrevOutput.GetData(mGrad))
      return false;
   mPGrad = mNoise.Transpose().MatMul(mGrad);
   mP = mP.Transpose();
   mP = (mP * (-1)).MatMul(mPGrad.MatMul(mP));

También trazaremos el gradiente de error a través de la forma de estabilización de Joseph.

//--- Joseph stabilized form for P
   if(!cPT.getGradient().Fill(mP))
      return false;
   if(!cP.CalcHiddenGradients(cPT.AsObject()))
      return false;
   if(!SumAndNormilize(cP.getGradient(), cPT.getGradient(), cP.getGradient(), 1, false, 0, 0, 0, 0.5f))
      return false;
//---
   if(!MatMulGrad(cK.getPrevOutput(), cKT.getPrevOutput(),
                  cKT.getOutput(), cKT.getGradient(),
                  cP.getGradient(), cKT.GetCount(),
                  cKT.GetWindow(), cKT.GetCount(), 1, true))
      return false;
   if(!MatMulGrad(cK.getOutput(), cK.getPrevOutput(),
                  cR_RT.getOutput(), cR_RT.getGradient(),
                  cKT.getPrevOutput(), cKT.GetCount(),
                  cRT.GetCount(), cRT.GetCount(), 1, true))
      return false;
   if(!MatMulGrad(cIdifK_H.getPrevOutput(), cIdifK_HT.getPrevOutput(),
                  cIdifK_HT.getOutput(), cIdifK_HT.getGradient(),
                  cP.getGradient(), cIdifK_HT.GetCount(),
                  cIdifK_HT.GetWindow(), cIdifK_HT.GetCount(), 1, true))
      return false;
   if(!MatMulGrad(cIdifK_H.getOutput(), cIdifK_H.getPrevOutput(),
                  cPPred.getOutput(), cPPred.getGradient(),
                  cIdifK_HT.getPrevOutput(), cIdifK_HT.GetCount(),
                  cIdifK_HT.GetWindow(), cIdifK_HT.GetWindow(), 1, true))
      return false;
//---
   if(!cK.CalcHiddenGradients(cKT.AsObject()))
      return false;
   if(!SumAndNormilize(cK.getGradient(), cK.getPrevOutput(), cK.getGradient(), 1, false, 0, 0, 0, 1))
      return false;

La matriz resultante se transmite a la cadena de propagación de gradiente en el bloque del filtro de Kalman y la inicializa. Primero, a través del módulo de actualización de mediciones.

//--- Measurement update
   if(!cIdifK_H.CalcHiddenGradients(cIdifK_HT.AsObject()))
      return false;
   if(!SumAndNormilize(cIdifK_H.getGradient(), cIdifK_H.getPrevOutput(), cIdifK_H.getGradient(),
                                                                          1, false, 0, 0, 0, 1))
      return false;
   if(!IdentDifferentGrad(cK_H.getGradient(), cIdifK_H.getGradient(), cHT.GetWindow(), 0, 0, 1))
      return false;
   if(!MatMulGrad(cK.getOutput(), cK.getPrevOutput(),
                  cH.getOutput(), cH.getGradient(),
                  cK_H.getGradient(), cKT.GetCount(),
                  cKT.GetWindow(), cHT.GetWindow(), 1, true))
      return false;
//---
   if(!MatMulGrad(cDeltY.getOutput(), cDeltY.getGradient(),
                  cK.getOutput(), cK.getPrevOutput(),
                  cX.getGradient(), 1,
                  cDeltY.Neurons(), cX.Neurons(), 1, true))
      return false;
   if(!SumAndNormilize(cK.getGradient(), cK.getPrevOutput(), cK.getGradient(), 1, false, 0, 0, 0, 1))
      return false;
   if(!DifferentGrad(cKoopmanPred.getGradient(), cYPred.getGradient(), cDeltY.getGradient(),
                     1, 0, 0, 0, 1))
      return false;
   if(!MatMulGrad(cXPred.getOutput(), cXPred.getGradient(),
                  cHT.getOutput(), cHT.getGradient(),
                  cYPred.getGradient(),
                  1, cHT.GetWindow(), cHT.GetCount(), 1, true))
      return false;
   if(!SumAndNormilize(cXPred.getGradient(), cX.getGradient(), cXPred.getGradient(), 1, false, 0, 0, 0, 1))
      return false;

A continuación, en el bloque de corrección, la propagación del gradiente pasa a través de las matrices de predicción y de error. Todos estos pasos construyen cuidadosamente el flujo del gradiente desde la salida hasta el espacio latente y, lo que resulta esencial, tienen en cuenta las relaciones estructurales entre las variables.

//--- Update step
   if(!MatMulGrad(cP_HT.getOutput(), cP_HT.getGradient(),
                  cSInv.getOutput(), cSInv.getGradient(),
                  cK.getGradient(), cHT.GetCount(),
                  (int)mS.Rows(), (int)mS.Cols(), 1, true))
      return false;
   if(cSInv.getGradient().GetData(mSGrad) <= 0)
      return false;
   mS = mS.Transpose();
   mS = (mS * (-1)).MatMul(mSGrad.MatMul(mS));
   if(cH_P_HT.getGradient().Fill(mS) <= 0)
      return false;
   if(!MatMulGrad(cH.getOutput(), cH.getPrevOutput(),
                  cP_HT.getOutput(), cP_HT.getPrevOutput(),
                  cH_P_HT.getGradient(), cHT.GetCount(),
                  cHT.GetWindow(), cHT.GetCount(), 1, true))
      return false;
   if(!SumAndNormilize(cH_P_HT.getGradient(), cR_RT.getGradient(),
                       cR_RT.getGradient(), int(mS.Cols()), false, 0, 0, 0, 1))
      return false;
   if(!SumAndNormilize(cH.getGradient(), cH.getPrevOutput(),
                       cH.getPrevOutput(), 1, false, 0, 0, 0, 1))
      return false;
   if(!SumAndNormilize(cP_HT.getGradient(), cP_HT.getPrevOutput(),
                       cP_HT.getGradient(), 1, false, 0, 0, 0, 1))
      return false;
   if(!MatMulGrad(cPPred.getOutput(), cPPred.getPrevOutput(),
                  cHT.getOutput(), cHT.getPrevOutput(),
                  cP_HT.getGradient(), cPPred.Neurons() / cHT.GetWindow(),
                  cHT.GetWindow(), cHT.GetCount(), 1, true))
      return false;
   if(!SumAndNormilize(cPPred.getGradient(), cPPred.getPrevOutput(),
                       cQ_QT.getGradient(), int(cQT.GetWindow()), false, 0, 0, 0, 1))
      return false;
   if(!SumAndNormilize(cHT.getGradient(), cHT.getPrevOutput(), cHT.getGradient(), 1, false, 0, 0, 0, 1))
      return false;

A continuación, se realiza un prediction step.

//--- Prediction step
   if(!MatMulGrad(cF_P.getOutput(), cF_P.getGradient(),
                  cFT.getOutput(), cFT.getGradient(),
                  cQ_QT.getGradient(), cFT.GetCount(),
                  cFT.GetWindow(), cFT.GetCount(), 1, true))
      return false;
   if(!MatMulGrad(cF.getOutput(), cF.getPrevOutput(),
                  cP.getOutput(), cP.getGradient(),
                  cF_P.getGradient(),
                  cFT.GetCount(), cFT.GetWindow(), cP.Neurons() / cFT.GetWindow(), 1, true))
      return false;
   if(!MatMulGrad(cAuxiliaryNet.getOutput(), cAuxiliaryNet.getGradient(),
                  cB.getOutput(), cB.getGradient(),
                  cXPred.getGradient(), 1,
                  cFT.GetWindow(), cFT.GetCount(), 1, true))
      return false;
   if(!MatMulGrad(NeuronOCL.getOutput(), NeuronOCL.getGradient(),
                  cFT.getOutput(), cFT.getPrevOutput(),
                  cXPred.getGradient(), 1,
                  cFT.GetWindow(), cFT.GetCount(), 1, true))
      return false;
   if(!SumAndNormilize(cFT.getGradient(), cFT.getPrevOutput(),
                       cFT.getGradient(), 1, false, 0, 0, 0, 1))
      return false;
//---
   if(!MatMulGrad(cR.getOutput(), cR.getPrevOutput(),
                  cRT.getOutput(), cRT.getGradient(),
                  cR_RT.getGradient(), cRT.GetCount(),
                  cRT.GetWindow(), cRT.GetCount(), 1, false))
      return false;
   if(!MatMulGrad(cQ.getOutput(), cQ.getPrevOutput(),
                  cQT.getOutput(), cQT.getGradient(),
                  cQ_QT.getGradient(), cQT.GetCount(),
                  cQT.GetWindow(), cQT.GetCount(), 1, false))
      return false;
   if(!cR.CalcHiddenGradients((CObject*)cRT.AsObject()))
      return false;
   if(!SumAndNormilize(cR.getGradient(), cR.getPrevOutput(),
                       cR.getGradient(), cRT.GetWindow(), false, 0, 0, 0, 0.01f))
      return false;
   if(!cQ.CalcHiddenGradients(cQT.AsObject()))
      return false;
   if(!SumAndNormilize(cQ.getGradient(), cQ.getPrevOutput(),
                       cQ.getGradient(), cQT.GetWindow(), false, 0, 0, 0, 0.01f))
      return false;
   if(!cH.CalcHiddenGradients(cHT.AsObject()))
      return false;
   if(!SumAndNormilize(cH.getGradient(), cH.getPrevOutput(),
                       cH.getGradient(), cHT.GetWindow(), false, 0, 0, 0, 0.01f))
      return false;
   if(!cF.CalcHiddenGradients(cFT.AsObject()))
      return false;
   if(!SumAndNormilize(cF.getGradient(), cF.getPrevOutput(),
                       cF.getGradient(), cFT.GetWindow(), false, 0, 0, 0, 0.01f))
      return false;

El último bloque se refiere a la representación en el espacio de Koopman y al módulo de atención. Aquí el gradiente se transfiere a las partes predichas y reconstruidas (KoopmanPred, KoopmanRest).

//--- Rest Attention
   if(!cKoopmanRest.CalcHiddenGradients(cAuxiliaryNet.AsObject()))
      return false;
//--- Pred / Rest
   if(!NeuronOCL.getPrevOutput().Fill(0))
      return false;
   if(!DifferentGrad(NeuronOCL.getPrevOutput(), cKoopmanRest.getPrevOutput(),
                     cKoopmanRest.getGradient(), cKoopman.GetWindow()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getPrevOutput(),
                       NeuronOCL.getPrevOutput(), 1, false, 0, 0, 0, 1))
      return false;
   if(NeuronOCL.Activation() != None)
     {
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getPrevOutput(),
                       NeuronOCL.getPrevOutput(), NeuronOCL.Activation()))
         return false;
     }
   if(!Concat(cKoopmanPred.getGradient(), cKoopmanRest.getPrevOutput(), cKoopman.getGradient(),
              cKoopman.GetWindow(), cKoopman.Neurons() - cKoopman.GetWindow(), 1))
      return false;

En este último caso, el gradiente se calcula mediante la diferencia, y luego ambas partes se combinan en una única estructura de Koopman.

Finalmente, el gradiente se transmite hasta el nivel de datos de origen.

//--- Koopman
   if(!NeuronOCL.CalcHiddenGradients(cKoopman.AsObject()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getPrevOutput(),
                       NeuronOCL.getGradient(), 1, false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

De este modo, el método construye paso a paso una ruta completa de propagación inversa que abarca todos los componentes clave del modelo: la parte latente probabilística, el filtrado de estado, la predicción, la corrección y la transformación en el espacio de Koopman. Todo esto garantiza un ajuste preciso de los parámetros del modelo y permite que se entrene con series temporales de forma eficiente.

El código fuente completo del objeto CNeuronK2VAEEncoder y todos sus métodos se ofrece en el archivo adjunto.

Hoy hemos realizado un trabajo extenso y minucioso, y el artículo ha adquirido un volumen considerable. Le sugiero tomarse un breve descanso para tener la oportunidad de asimilar el material y verlo con una perspectiva fresca. En el próximo artículo, completaremos lo empezado: examinaremos los puntos restantes en detalle y probaremos los enfoques implementados con datos históricos reales. Esto nos permitirá no solo consolidar la teoría, sino también evaluar su eficacia práctica.


Conclusión

En este artículo, hemos analizado con detalle la arquitectura y las principales etapas de implementación del Codificador en el framework K²VAE, que combina las capacidades de KoopmanNet y el filtro de Kalman en un único sistema para el análisis de series temporales. Este enfoque permite modelar de manera eficiente la dinámica de datos financieros complejos, combinando la previsión lineal clásica con un ajuste adaptativo flexible basado en observaciones. El framework aquí presentado demuestra claramente cómo los métodos tradicionales pueden integrarse a la perfección con las modernas tecnologías de redes neuronales, abriendo nuevas perspectivas en el análisis y la previsión de los mercados financieros.

En el próximo artículo, comenzaremos a realizar pruebas prácticas con el modelo con datos históricos reales para evaluar objetivamente su eficacia y potencial en condiciones reales de negociación.


Enlaces


Programas usados en el artículo

# Nombre Tipo Descripción
1 Study.mq5 Asesor Asesor de entrenamiento de modelos offline
2 StudyOnline.mq5 Asesor Asesor de entrenamiento de modelos online
3 Test.mq5 Asesor Asesor para la prueba de modelos
4 Trajectory.mqh Biblioteca de clases Estructura de descripción del estado del sistema y la arquitectura del modelo
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/18766

Archivos adjuntos |
MQL5.zip (2915.29 KB)
Red neuronal cuántica en MQL5 (Parte I): Creamos un archivo de inclusión Red neuronal cuántica en MQL5 (Parte I): Creamos un archivo de inclusión
El artículo presenta un nuevo enfoque para la creación de sistemas de negociación basados en principios cuánticos e inteligencia artificial. El autor describe el desarrollo de una red neuronal única que va más allá del aprendizaje automático clásico al integrar la mecánica cuántica con las arquitecturas de inteligencia artificial modernas.
Análisis de espectro singular (SSA) en MQL5 Análisis de espectro singular (SSA) en MQL5
Este artículo pretende servir de guía para aquellas personas que no estén familiarizadas con el concepto de análisis de espectro singular (SSA) y que deseen adquirir los conocimientos necesarios para poder aplicar las herramientas integradas disponibles en MQL5.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Del básico al intermedio: Acceso aleatorio (II) Del básico al intermedio: Acceso aleatorio (II)
En este artículo, veremos cómo dos enfoques ligeramente diferentes pueden impactar de manera considerable en toda una metodología de implementación, tanto desde el punto de vista del rendimiento como desde el punto de vista de cómo deben pensarse los accesos al disco, con el fin de evitar problemas de compatibilidad entre distintas aplicaciones.