Redes neuronales en el trading: Modelo multidimensional de extremo a extremo para la previsión de series temporales (Componentes principales)
Introducción
En nuestro artículo anterior, analizamos con detalle el framework GinAR, una arquitectura moderna para trabajar con series temporales que combina las ventajas de las redes neuronales de grafos con la capacidad de entrenar con datos asíncronos, incompletos y heterogéneos. La idea principal de este enfoque consiste en interpretar las series temporales no como una secuencia plana de observaciones, sino como una estructura de grafo, donde las variables individuales o los puntos temporales pueden vincularse mediante interdependencias arbitrarias y aprendibles. Esta perspectiva resulta especialmente relevante para las tareas de modelado financiero, donde los datos suelen ser irregulares, con lagunas y relaciones latentes complejas.
Para resolver este tipo de problemas, propusimos GinAR como herramienta universal. Su estructura modular incluye varios componentes clave:
- un mecanismo de Interpolation Attention (atención por interpolación), que permite reconstruir los valores faltantes usando la agregación de información de las variables observadas;
- capas de grafos con pesos entrenables individualmente;
- funciones de normalización adaptativas que garantizan la robustez del modelo frente a valores atípicos e inestabilidad de escala.
El mecanismo de Interpolation Attention ocupa un lugar central en la arquitectura. No se trata de un núcleo de Self-Attention clásico, sino de un módulo adaptativo completo capaz de considerar tanto las dependencias globales entre variables como el contexto de observación local. En la práctica, esto significa que el modelo es capaz de, por ejemplo, predecir el valor de un indicador financiero, incluso en ausencia de sus últimas mediciones, usando la estructura de los indicadores vecinos y el panorama general del mercado. Este enfoque resulta fundamental en entornos reales donde los datos a menudo llegan tarde, de forma desigual o con grandes lagunas.
La característica distintiva de GinAR es su capacidad para reconfigurar dinámicamente la estructura del grafo durante el entrenamiento. A diferencia de la mayoría de los modelos de grafos, donde la estructura está fijada de antemano, aquí se conforma durante el proceso de aprendizaje. Esto permite tener en cuenta las condiciones cambiantes del mercado, las correlaciones y los factores ocultos. El modelo decide de forma independiente qué variables deben vincularse entre sí, cuáles deben debilitarse y cuáles deben destacarse como clave para el contexto actual. Esto crea una arquitectura flexible que puede adaptarse a los regímenes y dinámicas del mercado.
A continuación le presentamos la visualización del framework GinAR realizada por el autor.

En la parte práctica del artículo anterior, realizamos una cantidad significativa de trabajo, sentando las bases para los cálculos en el programa OpenCL. Asimismo, implementamos todas las funciones clave, desde reducciones locales y cálculos SoftMax hasta las pasadas directa y inversa en el módulo de Interpolation Attention. Además, prestamos especial atención al manejo correcto de la memoria local, la sincronización de flujos y la estabilidad numérica, lo cual es especialmente importante al trabajar con series temporales incompletas en condiciones de computación paralela.
Esta preparación nos abre el camino a la siguiente etapa: la integración del núcleo del modelo en el programa principal. Aquí es donde el algoritmo cobra vida: así, obtendremos los datos del entorno de trading, los procesaremos mediante dispositivos OpenCL, los pasaremos a través del modelo y los devolveremos como pronósticos y decisiones comerciales. Este puente entre la lógica de alto nivel y la computación acelerada de bajo nivel supone una parte fundamental de toda nuestra implementación de la arquitectura GinAR.
Interpolation Attention
Hoy realizaremos una gran cantidad de trabajo práctico relacionado con la implementación de componentes clave del framework GinAR. Por lo tanto, no nos detendremos mucho en la teoría; iremos directamente al grano.
El primer paso consiste en crear el módulo más importante, el módulo de Interpolation Attention, que diseñaremos como una clase CNeuronInterpolationAttention aparte. Esta clase hereda del objeto de capa neuronal básico de nuestra biblioteca CNeuronBaseOCL e implementa toda la lógica necesaria para utilizar eficazmente los núcleos OpenCL preparados previamente en un ciclo completo de pasadas directas y inversas.
class CNeuronInterpolationAttention : public CNeuronBaseOCL { protected: //--- uint iCount; uint iDimension; //--- CParams cW; CParams cA; CParams cGL; //--- CNeuronBaseOCL cH; CNeuronBaseOCL cAdj; CNeuronBaseOCL cAttention; //--- virtual bool InterpolationAttention(CNeuronBaseOCL *NeuronOCL); virtual bool InterpolationAttentionGrad(CNeuronBaseOCL *NeuronOCL); virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronInterpolationAttention(void) { activation = None; } ~CNeuronInterpolationAttention(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint dimension, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(const int file_handle) override; virtual bool Load(const int file_handle) override; //--- virtual int Type(void) override const { return defNeuronInterpolationAttention; } virtual void TrainMode(bool flag) override; virtual void SetOpenCL(COpenCLMy *obj); //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau); virtual void SetActivationFunction(ENUM_ACTIVATION value) override {}; };
La clase contiene un conjunto de variables internas, entre las cuales iCount e iDimension son responsables del número de elementos en el tensor de datos de origen, mientras que los objetos cW, cA y cGL representan parámetros matriciales entrenables. Los objetos intermedios se definen por separado para almacenar los resultados intermedios de las representaciones ocultas, el grafo de adyacencia y los pesos de atención.
La lógica computacional principal está encapsulada en los métodos de InterpolationAttention e InterpolationAttentionGrad, que son responsables de la pasada directa e inversa del modelo, respectivamente. Se encargan de preparar y poner en cola la ejecución de los núcleos de programa OpenCL correspondientes que creamos como parte del trabajo práctico del artículo anterior. Estos usan un algoritmo que ya nos resulta familiar, por lo que no nos detendremos en él en este artículo. El código completo de estos métodos se ofrece en el archivo adjunto para su estudio independiente.
Todos los objetos internos de la clase CNeuronInterpolationAttention se declaran de forma estática, lo cual simplifica la gestión del ciclo de vida de la instancia. Esto permite dejar vacíos el constructor y el destructor de la clase, ya que no requieren inicialización explícita ni liberación de recursos. Toda la configuración necesaria, incluida la inicialización de los miembros propios de la clase padre y los componentes heredados, se realiza de manera centralizada en el método Init. Esto ofrece una estructura de inicialización limpia y predecible, evitando la duplicación de código y aumentando la fiabilidad al reutilizar el módulo en diferentes configuraciones de modelo.
bool CNeuronInterpolationAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint dimension, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_count * dimension, optimization_type, batch)) return false;
En el cuerpo del método, primero llamamos al método homónimo en la clase padre CNeuronBaseOCL. En este caso, el tamaño de la capa se recalcula considerando la dimensionalidad espacial de la señal original.
A continuación, se guardan los parámetros clave del modelo (número de variables y dimensionalidad de representación) para su uso en cálculos y gestión de búferes internos. Estos parámetros determinan el tamaño de todos los demás vectores y matrices que se utilizarán durante las pasadas directa y inversa.
iCount = units_count; iDimension = dimension;
El siguiente bloque de código consiste en la inicialización de los parámetros y tensores intermedios que intervienen en el mecanismo de Interpolation Attention. En primer lugar, se crean los pesos entrenables cW, que representan la matriz de interacción individual entre variables. Esta es una matriz cuadrada de dimension × dimension.
int index = 0; if(!cW.Init(0, index, OpenCL, iDimension * iDimension, optimization, iBatch)) return false;
Luego se inicializan los parámetros cA, que son los responsables de la agregación de la atención entre los nodos.
index++; if(!cA.Init(0, index, OpenCL, 2 * iDimension, optimization, iBatch)) return false; index++; if(!cGL.Init(0, index, OpenCL, iDimension * iDimension, optimization, iBatch)) return false;
Posteriormente, se crea un objeto cGL que representa los vectores de variables latentes. También tiene las dimensionalidades dimension × dimension.
Tras inicializar todos los parámetros de entrenamiento, comienza la configuración de los objetos auxiliares encargados de almacenar los resultados intermedios. El búfer cH representa una matriz de transformaciones lineales de la señal original, obtenida al multiplicar la entrada por pesos. Su dimensionalidad es units_count × dimension.
index++; if(!cH.Init(0, index, OpenCL, iCount * iDimension, optimization, iBatch)) return false;
A continuación, viene el objeto cAdj, que almacena los valores de correlación ajustados entre las variables.
index++; if(!cAdj.Init(0, index, OpenCL, iDimension * iDimension, optimization, iBatch)) return false; index++; if(!cAttention.Init(0, index, OpenCL, iDimension * iDimension, optimization, iBatch)) return false; //--- return true; }
Finalmente, cAttention es el tensor de valores de atención finales obtenidos después de la normalización usando SoftMax.
Cada inicialización va acompañada de una comprobación acerca del éxito. Si falla algún paso, el método retorna inmediatamente false, lo que ofrece una protección fiable contra errores de configuración.
Si todos los objetos se configuran correctamente y quedan alojados en la memoria del dispositivo OpenCL, el método finaliza y devuelve true. Esto significa que la capa está completamente lista para ejecutarse y puede usarse como parte del modelo.
Una vez finalizado el trabajo de inicialización del objeto, pasamos a organizar el proceso de pasada directa, que implementamos en el método feedForward. Su tarea consiste en preparar correctamente todos los datos iniciales y ejecutar el cálculo principal utilizando el núcleo OpenCL previamente desarrollado. El algoritmo es bastante compacto, pero incluye varias etapas clave, cada una de las cuales desempeña una función importante en el esquema general del funcionamiento de la capa.
bool CNeuronInterpolationAttention::feedForward(CNeuronBaseOCL *NeuronOCL) { if(bTrain) { if(!cW.FeedForward()) return false; if(!cA.FeedForward()) return false; if(!cGL.FeedForward()) return false; } if(!InterpolationAttention(NeuronOCL)) return false; //--- return true; }
En primer lugar, el método comprueba si el indicador bTrain está activo, lo cual significa que la capa está en modo de entrenamiento. Esto resulta fundamental porque, en este caso, los parámetros entrenables del modelo deben procesarse primero mediante sus propios métodos FeedForward. Esta llamada es necesaria para actualizar sus valores y garantizar su participación en el ciclo de cálculo actual. Las tres llamadas van acompañadas de una comprobación de éxito, y si se produce el más mínimo fallo, el método finaliza inmediatamente su ejecución, devolviendo false. Esto evita que parámetros no inicializados o incorrectos se incluyan en cálculos posteriores.
Una vez finalizados todos los pasos preparatorios, se llama al método principal InterpolationAttention. Esto es lo que inicializa el lanzamiento del kernel de OpenCL, encargado de calcular la atención, agregar valores y generar la representación de salida de la capa.
El método finaliza su trabajo devolviendo el resultado lógico de las operaciones realizadas al programa que realiza la llamada.
El método calcInputGradients está implementado de forma extremadamente concisa, ya que toda la lógica para la propagación inversa de errores ya se ha trasladado al kernel OpenCL correspondiente. Aquí simplemente llamamos a la función contenedora InterpolationAttentionGrad, que es la responsable de iniciar el kernel y pasar los búferes necesarios. Y devolvemos el resultado lógico de las operaciones al programa que realiza la llamada.
bool CNeuronInterpolationAttention::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!InterpolationAttentionGrad(NeuronOCL)) return false; //--- return true; }
Esta implementación conserva por completo la estructura general de las capas neuronales, lo que permite usar este módulo junto con otros tipos de capas, sin modificar la lógica de aprendizaje. Esto pone de relieve la flexibilidad arquitectónica del sistema y hace que la integración de nuevas soluciones resulte lo más sencilla y segura posible.
La actualización de los parámetros de entrenamiento en el módulo se implementa de la forma más compacta y lógica posible. Todos los pesos involucrados en los cálculos son objetos de clase internos (cW, cA, cGL), cada uno de los cuales encapsula su propio algoritmo de actualización. El método updateInputWeights simplemente llama a las funciones correspondientes, lo cual garantiza la modularidad y la claridad de la arquitectura.
bool CNeuronInterpolationAttention::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cW.UpdateInputWeights()) return false; if(!cA.UpdateInputWeights()) return false; if(!cGL.UpdateInputWeights() || !Normilize(cGL.getWeightsParams(), 2 * iDimension)) return false; //--- return true; }
Se presta especial atención al tensor cGL, que tras su actualización se normaliza adicionalmente usando la función Normilize. Esto es necesario porque cGL contiene los pesos de las correlaciones entre variables, y la estabilidad de estos coeficientes afecta directamente a la estabilidad de todo el módulo de atención. La normalización permite suavizar los posibles valores atípicos y mantener la interpretabilidad de los pesos. De esta forma, este método completa por completo el ciclo de aprendizaje sin necesidad de intervenciones ni configuraciones adicionales.
El código completo de la clase CNeuronInterpolationAttention, así como la implementación de todos sus métodos, se pueden encontrar en los archivos adjuntos. Hemos evitado deliberadamente sobrecargar este artículo con detalles técnicos para que la presentación resulte amena. Sin embargo, le recomendamos estudiar el código fuente para comprender al completo la lógica del módulo y su integración en el framework general de GinAR.
AGCN
El siguiente paso lógico en la construcción del framework GinAR es la implementación del mecanismo de convolución de grafos adaptativa (AGCN). Este es un elemento clave del framework, que nos permite tener en cuenta las relaciones complejas entre series temporales representadas como nodos de un grafo. A diferencia de los enfoques tradicionales, donde la estructura del grafo se especifica de antemano y permanece sin cambios durante el entrenamiento, la convolución adaptativa permite que el modelo forme y cambie de manera independiente la topología de las conexiones según el estado actual de los datos. Esta flexibilidad resulta fundamental al trabajar con series temporales financieras dinámicas, cuya estructura está sujeta a fluctuaciones constantes, relaciones ocultas y perturbaciones externas.
Para implementar esta idea, se desarrolló un módulo independiente, presentado como la clase CNeuronAGCN. Esta clase hereda de la capa básica totalmente conectada CNeuronBaseOCL y sirve como encapsulación de toda la lógica necesaria para realizar convolución adaptativa utilizando la aceleración OpenCL. Su estructura interna está diseñada para garantizar la máxima eficiencia computacional y precisión en cada etapa del procesamiento, incluyendo la extracción de características, la formación de la matriz de conexión, la normalización, la agregación y el entrenamiento de los pesos del grafo.
La característica distintiva de este módulo es que no se basa en una matriz de adyacencia fija al procesar la información, sino que la calcula en tiempo real. Para ello, los datos de origen pasan por una secuencia de transformaciones, lo cual da como resultado la formación de una representación latente de cada nodo. Estas representaciones se usan para calcular la similitud entre nodos, que a su vez se utiliza para construir la matriz de relaciones adaptativa. De esta forma, el modelo analiza cada serie temporal y decide por sí mismo con qué otras series debe vincularse, y con qué intensidad. El resultado es un mecanismo de atención flexible en el que cada nodo es capaz de adaptar su comportamiento en función del contexto.
A continuación, le mostramos la estructura de la nueva clase.
class CNeuronAGCN : public CNeuronBaseOCL { protected: CParams cEa; CNeuronSwiGLUOCL cWx; CNeuronSwiGLUOCL cWe; CNeuronBaseOCL cWconcat_ex; CNeuronConvOCL cEn; CNeuronTransposeOCL cEnT; CNeuronBaseOCL cEnEnT; CNeuronSoftMaxOCL cAadapt; CNeuronBaseOCL cAadaptX; CNeuronBaseOCL cApreX; CNeuronSwiGLUOCL cWadapt; CNeuronSwiGLUOCL cWpre; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *second) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override; public: CNeuronAGCN(void) {activation = None;} ~CNeuronAGCN(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint dimension, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(const int file_handle) override; virtual bool Load(const int file_handle) override; //--- virtual int Type(void) override const { return defNeuronAGCN; } virtual void TrainMode(bool flag) override; virtual void SetOpenCL(COpenCLMy *obj); //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau); //--- virtual uint GetWindow(void) const { return cWx.GetWindow(); } virtual uint GetUnits(void) const { return cWx.GetUnits(); } virtual void SetActivationFunction(ENUM_ACTIVATION value) override {}; };
La cadena computacional al completo está organizada dentro de un único objeto, pero incluye muchas capas y operaciones auxiliares. La lógica se divide en pasos; nos familiarizaremos con ellos a medida que implementemos los métodos de la clase. Las características resultantes pueden interpretarse como el resultado de un filtrado adaptado al grafo, considerando la información sustantiva y las relaciones topológicas entre las series temporales.
Al igual que en la capa anterior, todos los componentes internos de la clase CNeuronAGCN se declararon de forma estática. Este enfoque nos permite dejar vacíos el constructor y el destructor de la clase, concentrando toda la lógica de inicialización en un solo método: Init. Esto nos garantiza la compacidad de la implementación y también facilita el control del ciclo de vida de los objetos dentro de la capa.
bool CNeuronAGCN::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint dimension, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_count * dimension, optimization_type, batch)) return false;
La inicialización comienza con una llamada al método homónimo de la clase padre, donde se realiza la configuración inicial de las interfaces de la capa y los parámetros generales. Posteriormente, todos los componentes clave de la convolución adaptativa se configuran paso a paso.
En el primer paso, se crea un objeto cEa, que se usará para codificar información adicional sobre la estructura de datos.
//--- int index = 0; if(!cEa.Init(0, index, OpenCL, Neurons(), optimization, iBatch)) return false; cEa.SetActivationFunction(None);
A continuación, se inicializan dos capas importantes: cWx y cWe, que se encargan de extraer características de los datos de origen y de la estructura de la serie temporal aprendida previamente. Ambas capas se implementan basándose en el mecanismo SwiGLU y se entrenan en paralelo.
index++; if(!cWx.Init(0, index, OpenCL, dimension, dimension, dimension, units_count, 1, optimization, iBatch)) return false; index++; if(!cWe.Init(0, index, OpenCL, dimension, dimension, dimension, units_count, 1, optimization, iBatch)) return false;
A continuación, se incluye un búfer cWconcat_ex, que posibilita la unión de las representaciones en el espacio latente.
index++; if(!cWconcat_ex.Init(0, index, OpenCL, 2 * Neurons(), optimization, iBatch)) return false; cWconcat_ex.SetActivationFunction(None);
El siguiente elemento de inicialización es la capa cEn, que realiza una transformación lineal de las características combinadas. Se complementa con un módulo de transposición cEnT, que prepara los datos para la formación de una matriz de atención preliminar.
index++; if(!cEn.Init(0, index, OpenCL, 2 * dimension, 2 * dimension, dimension, units_count, 1, optimization, iBatch)) return false; cEn.SetActivationFunction(None); index++; if(!cEnT.Init(0, index, OpenCL, units_count, dimension, optimization, iBatch)) return false; index++; if(!cEnEnT.Init(0, index, OpenCL, units_count * units_count, optimization, iBatch)) return false; cEnEnT.SetActivationFunction(GELU);
Tras el paso de transposición, los datos preparados se introducen en una operación de multiplicación de matrices, donde se produce un producto escalar de la representación de origen con su versión transpuesta. Como resultado, se forma una matriz simétrica de relaciones, que se coloca en el objeto cEnEnT. Precisamente en este bloque se realiza la activación de los valores obtenidos mediante la función GELU, que permite la supresión gradual de las correlaciones negativas, sin eliminarlas por completo, pero reduciendo simultáneamente su contribución a la representación final.
Este enfoque ayuda a identificar los patrones más significativos y las conexiones latentes entre los nodos del grafo, lo que resulta fundamental al formar una matriz de atención adaptativa. En definitiva, esta operación permite al modelo capturar patrones estructurales más profundos dentro del grafo y ponderar correctamente las interacciones entre nodos.
Luego viene la etapa clave: la normalización de los valores obtenidos. Esto se realiza usando la función SoftMax, que se aplica dentro del objeto cAadapt. Esta operación transforma los pesos brutos en una distribución de probabilidad donde a cada conexión entre los nodos del grafo se le asigna un cierto grado de importancia. De esta forma, se suprimen las dependencias débiles y ruidosas, mientras que se potencian las fuertes y informativas. El resultado es una matriz de atención adaptativa completa que refleja los patrones individuales de interacción entre los componentes de la señal de entrada.
index++; if(!cAadapt.Init(0, index, OpenCL, units_count * units_count, optimization, iBatch)) return false; cAadapt.SetHeads(units_count);
La siguiente operación lógica es la aplicación de ponderaciones adaptativas a los datos de origen. Esta etapa se implementa ponderando las características de entrada usando los coeficientes de atención correspondientes. El resultado se almacena en el búfer cAadaptX, que supone una representación actualizada y rica en contexto de la señal de entrada. En esencia, se trata de una proyección renormalizada de la información original, que considera las relaciones identificadas entre los nodos individuales del grafo.
index++; if(!cAadaptX.Init(0, index, OpenCL, Neurons(), optimization, iBatch)) return false; cAadaptX.SetActivationFunction(None); index++; if(!cApreX.Init(0, index, OpenCL, Neurons(), optimization, iBatch)) return false; cApreX.SetActivationFunction(None);
Una etapa de procesamiento adicional consiste en adaptar los datos de origen a una estructura gráfica predefinida. Este paso tiene como objetivo mejorar la influencia de las conexiones conocidas y predefinidas entre nodos obtenidas fuera del modelo de entrenamiento; por ejemplo, basándose en conexiones fundamentales, topología o reglas de expertos. El mecanismo en este caso es similar al anterior: se aplican pesos al tensor de origen, pero ya no a partir de la matriz de atención adaptativa, sino de la matriz de coeficientes externa. El resultado se almacena en el búfer cApreX. De esta forma, el modelo recibe dos canales de información independientes a la vez: uno basado en el autoaprendizaje y los patrones identificados, y el segundo en información estructural a priori. Su combinación permite un mayor equilibrio entre flexibilidad y robustez en el modelo.
Finalmente, en la parte final de la inicialización, se crean dos capas de convolución: cWadapt y cWpre. Ambas utilizan la arquitectura SwiGLU, que actúa como filtros que adaptan las características a la estructura de la matriz de conexión generada.
index++; if(!cWadapt.Init(0, index, OpenCL, dimension, dimension, dimension, units_count, 1, optimization, iBatch)) return false; cWadapt.SetActivationFunction(None); index++; if(!cWpre.Init(0, index, OpenCL, dimension, dimension, dimension, units_count, 1, optimization, iBatch)) return false; cWpre.SetActivationFunction(None); SetActivationFunction(None); //--- return true; }
Una vez completados todos estos pasos, desactivamos forzosamente la función de activación de la capa, ya que las no linealidades se aplican dentro de los bloques internos y no resulta necesario duplicarlas en la salida.
Por lo tanto, todo el método Init supone una secuencia de pasos cuidadosamente orquestada durante la cual se crean y configuran todos los elementos de convolución adaptativa. Cada componente es responsable de una parte específica de los cálculos y se integra en la arquitectura general de la capa, creando la base para el funcionamiento posterior del modelo.
La siguiente etapa de nuestro trabajo es el método feedForward, que implementa un ciclo completo de pasada directa de la convolución adaptativa de grafos, considerando tanto las conexiones estructurales entrenadas como las especificadas externamente. El algoritmo es bastante complejo, así que lo examinaremos paso a paso, centrándonos en la lógica subyacente de cada bloque.
bool CNeuronAGCN::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!SecondInput || SecondInput.Total() < cAadapt.Neurons()) return false;
Antes de que el algoritmo comience a ejecutarse, se realiza una comprobación obligatoria de la corrección de los datos de entrada: si falta la matriz de coeficientes de correlación predefinidos SecondInput, o si su tamaño resulta insuficiente para realizar operaciones matriciales, la ejecución se interrumpe de inmediato.
Luego, si el modelo está en modo de entrenamiento, se inicia un ciclo de preparación de parámetros para la rama responsable del contexto adaptativo de los nodos. En primer lugar, se realiza una pasada directa sobre el objeto cEa, que contiene los parámetros entrenables para calcular las incorporaciones de las aristas. El resultado obtenido se transfiere a cWe, donde se forma una matriz de pesos de interacción externa que refleja el contexto de las conexiones entre nodos en el paso de entrenamiento actual.
if(bTrain) { if(!cEa.FeedForward()) return false; if(!cWe.FeedForward(cEa.AsObject())) return false; }
Paralelamente, se activa la rama principal, la que trabaja con la secuencia principal de características. En ella, se llama al método de pasada directa cWx, que genera una representación del estado actual de los nodos.
if(!cWx.FeedForward(NeuronOCL)) return false; if(!Concat(cWx.getOutput(), cWe.getOutput(), cWconcat_ex.getOutput(), cWx.GetWindowOut(), cWe.GetWindowOut(), cWx.GetUnits())) return false;
Posteriormente, ambos vectores de salida (cWx y cWe) se concatenan a través de los elementos individuales de la secuencia, lo que permite que el modelo tenga en cuenta la información estructural y contextual en las operaciones posteriores. El resultado obtenido se utiliza en cEn, que implementa una convolución lineal que reduce la dimensionalidad y realza las características significativas.
if(!cEn.FeedForward(cWconcat_ex.AsObject())) return false; if(!cEnT.FeedForward(cEn.AsObject())) return false; if(!MatMul(cEn.getOutput(), cEnT.getOutput(), cEnEnT.getOutput(), cEnT.GetCount(), cEnT.GetWindow(), cEnT.GetCount())) return false; if(cEnEnT.Activation() != None) if(!Activation(cEnEnT.getOutput(), cEnEnT.getOutput(), cEnEnT.Activation())) return false;
A continuación, se produce la operación clave: la transposición de la matriz resultante y su multiplicación por la original, lo cual nos permite formar una matriz de correlación simétrica en el objeto cEnEnT. Si se aplica una función de activación (en nuestro caso, GELU) a este objeto, se invoca su llamada, mitigando el impacto del ruido y las correlaciones negativas.
La matriz resultante se pasa luego al objeto cAadapt, donde se aplica el mecanismo SoftMax usando una estructura de atención multi-cabeza. Esta normaliza los pesos entre las filas y devuelve una matriz de atención adaptada a la estructura de datos actual. Para reforzar las conexiones diagonales, se llama a la función IdentSum, que añade la matriz diagonal identidad al resultado, lo que garantiza que el grafo sea robusto ante entradas dispersas.
if(!cAadapt.FeedForward(cEnEnT.AsObject())) return false; if(!IdentSum(cAadapt.getOutput(), cAadapt.getOutput(), cAadapt.Heads())) return false;
A continuación, el modelo calcula dos secuencias paralelas de multiplicación de matrices. El primer flujo opera sobre los datos externos SecondInput, multiplicándolos por las salidas actuales del bloque; el resultado se almacena en el búfer cApreX. El segundo flujo opera de forma similar sobre la matriz de atención cAadapt y las salidas actuales, almacenando el resultado en cAadaptX.
if(!MatMul(SecondInput, NeuronOCL.getOutput(), cApreX.getOutput(), cWpre.GetUnits(), cWpre.GetUnits(), cWpre.GetWindow())) return false; if(!MatMul(cAadapt.getOutput(), NeuronOCL.getOutput(), cAadaptX.getOutput(), cWadapt.GetUnits(), cWadapt.GetUnits(), cWadapt.GetWindow())) return false;
El último paso consiste en pasar ambos flujos a través de los bloques convolucionales entrenables cWpre y cWadapt. Esto permite que el modelo matice los datos obtenidos desde el punto de vista de las características locales.
if(!cWpre.FeedForward(cApreX.AsObject())) return false; if(!cWadapt.FeedForward(cAadaptX.AsObject())) return false; if(!SumAndNormilize(cWadapt.getOutput(), cWpre.getOutput(), Output, cWadapt.GetWindowOut(), true)) return false; //--- return true; }
Después de esto, se llama a la función SumAndNormilize, que combina ambos flujos y da como resultado el formato requerido en la salida de la capa.
De este modo, el método implementa varios mecanismos potentes a la vez: atención adaptativa, extracción de características, integración estructural y transformación convolucional. Todo ello convierte al módulo en una unidad de procesamiento de grafos completa, capaz de tener en cuenta tanto las dependencias internas entre los nodos como las restricciones externas de la estructura.
Tras completar la pasada directa en la convolución de grafos adaptativa (AGCN), comienza una de las etapas más críticas: la propagación de los gradientes de error desde la salida de la capa hasta sus componentes entrenables. Este proceso implementa la propagación inversa (backpropagation), lo que nos permite determinar con precisión la contribución de cada objeto al resultado final y ajustar los parámetros, minimizando el error. Todo el algoritmo está organizado dentro del método calcInputGradients.
bool CNeuronAGCN::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -1) { if(!NeuronOCL || !SecondInput || !SecondGradient || SecondInput.Total() < cAadapt.Neurons() || SecondGradient.Total() < cAadapt.Neurons()) return false;
En la primera fase, se realiza una comprobación estándar de la validez de los datos de entrada: si los punteros obtenidos no están inicializados o el tamaño de los búferes de entrada es insuficiente para realizar operaciones matriciales, la ejecución del método se detiene. A continuación, comienza la distribución real de los errores.
La propagación inversa en el bloque AGCN comienza desde el final, es decir, desde la capa de salida, donde el resultado final ya ha sido formado. En nuestro caso, esta es la suma de las salidas de dos componentes clave: cWadapt y cWpre. Son ellos, permítanme recordarles, quienes participan en la construcción de la representación final, que generaliza tanto la atención como la estructura estática del grafo.
Como la salida de un bloque es una simple suma de dos tensores, el gradiente de error obtenido de la capa subsiguiente debe pasarse por igual a ambos módulos, sin ningún coeficiente de peso adicional. Sin embargo, antes de comenzar a calcular los gradientes para las conexiones internas de cada módulo, debemos corregir este gradiente considerando las funciones de activación aplicadas en la pasada directa. Para ello, se llama al método DeActivation; esencialmente, multiplica el gradiente resultante por la derivada de la función de activación aplicada a la salida de cWadapt y cWpre. Este es un paso clave, sin el cual la propagación inversa a través de transformaciones no lineales será incorrecta.
if(!DeActivation(cWadapt.getOutput(), cWadapt.getGradient(), Gradient, cWadapt.Activation())) return false; if(!DeActivation(cWpre.getOutput(), cWpre.getGradient(), Gradient, cWpre.Activation())) return false;
Después de esto, el error se distribuye entre sus entradas: se llaman los métodos CalcHiddenGradients para los objetos cAadaptX y cApreX a través de los cuales se pasaron los flujos formados considerando las matrices de conexiones adaptativas y predefinidas entre nodos, respectivamente.
if(!cApreX.CalcHiddenGradients(cWpre.AsObject())) return false; if(!cAadaptX.CalcHiddenGradients(cWadapt.AsObject())) return false;
A continuación, MatMulGrad realiza la propagación inversa mediante una operación de multiplicación: se calcula la contribución de cada parte al error final. En caso de usar la función de activación, su transformación inversa se realiza mediante DeActivation, lo cual permite restaurar el error real antes de su normalización.
if(!MatMulGrad(SecondInput, SecondGradient, NeuronOCL.getOutput(), NeuronOCL.getGradient(), cApreX.getGradient(), cWpre.GetUnits(), cWpre.GetUnits(), cWpre.GetWindow())) return false; if(SecondActivation != None) if(!DeActivation(SecondInput, SecondGradient, SecondGradient, SecondActivation)) return false; //--- if(!MatMulGrad(cAadapt.getOutput(), cAadapt.getGradient(), NeuronOCL.getOutput(), PrevOutput, cAadaptX.getGradient(), cWadapt.GetUnits(), cWadapt.GetUnits(), cWadapt.GetWindow())) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), PrevOutput, PrevOutput, cWx.GetWindow(), false)) return false; if(NeuronOCL.Activation() != None) if(!DeActivation(NeuronOCL.getOutput(), PrevOutput, PrevOutput, NeuronOCL.Activation())) return false;
Tenga en cuenta que ambas operaciones de multiplicación de matrices en la pasada directa utilizaron los datos del flujo de datos principal original. Por consiguiente, necesitamos recopilar el gradiente de error a lo largo de ambas líneas troncales. Para acumular los valores, utilizamos un búfer temporal PrevOutput. Si NeuronOCL usa alguna función de activación, su derivada se utiliza para recalcular correctamente el gradiente teniendo en cuenta la no linealidad.
El siguiente paso clave es la propagación inversa a través de la matriz de correlación cEnEnT obtenida anteriormente en el paso de pasada directa. En primer lugar, se calculan los gradientes ocultos y, a continuación, se produce la aplicación inversa de la función de activación en la salida si se ha usado la función GELU.
if(!cEnEnT.CalcHiddenGradients(cAadapt.AsObject())) return false; if(cEnEnT.Activation() != None) if(!DeActivation(cEnEnT.getOutput(), cEnEnT.getGradient(), cEnEnT.getGradient(), cEnEnT.Activation())) return false;
Después de esto, se llama a MatMulGrad, que implementa la propagación inversa del gradiente a través de la operación de multiplicación de las características y su copia transpuesta.
if(!MatMulGrad(cEn.getOutput(), cEn.getPrevOutput(), cEnT.getOutput(), cEnT.getGradient(), cEnEnT.getGradient(), cEnT.GetCount(), cEnT.GetWindow(), cEnT.GetCount())) return false;
Además, los errores transpuestos se pasan a la capa cEn, donde se suman a los valores obtenidos previamente y se desactivan si se utilizó una función de activación.
if(!cEn.CalcHiddenGradients(cEnT.AsObject())) return false; if(!SumAndNormilize(cEn.getGradient(), cEn.getPrevOutput(), cEn.getGradient(), cEnT.GetWindow(), false)) return false; if(cEn.Activation() != None) if(!DeActivation(cEn.getOutput(), cEn.getGradient(), cEn.getGradient(), cEn.Activation())) return false;
Inmediatamente después, los gradientes se envían a cWconcat_ex, donde comienza el procedimiento de separación de características inversas (recuerde que se combinaron en la pasada directa).
if(!cWconcat_ex.CalcHiddenGradients(cEn.AsObject())) return false; if(!DeConcat(cWx.getGradient(), cWe.getGradient(), cWconcat_ex.getGradient(), cWx.GetWindowOut(), cWe.GetWindowOut(), cWx.GetUnits())) return false; if(cWx.Activation() != None) if(!DeActivation(cWx.getOutput(), cWx.getGradient(), cWx.getGradient(), cWx.Activation())) return false; if(cWe.Activation() != None) if(!DeActivation(cWe.getOutput(), cWe.getGradient(), cWe.getGradient(), cWe.Activation())) return false;
La función DeConcat separa correctamente los gradientes en dos partes: para cWx y cWe. En cada uno de estos módulos, se llama a DeActivation por separado, restaurando el gradiente limpio. Luego se produce la propagación inversa a través de la red: primero al nivel de datos de origen a través de `cWx`, luego a cEa a través de cWe.
if(!NeuronOCL.CalcHiddenGradients(cWx.AsObject())) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), PrevOutput, NeuronOCL.getGradient(), cWx.GetWindow(), false)) return false; if(!cEa.CalcHiddenGradients(cWe.AsObject())) return false; //--- return true; }
Tenga en cuenta que ya hemos transmitido el gradiente de error al nivel de los datos de origen de la línea troncal, por lo que sumamos los valores obtenidos con los previamente acumulados.
Cabe destacar que el método implementa una secuencia estricta de todos los pasos. Esto es necesario porque el error debe rebobinarse estrictamente en el orden inverso al que se produjo la pasada directa. Cada paso compensa cuidadosamente los efectos de las convoluciones, normalizaciones, activaciones y transposiciones. Y solo al final, una vez que se han calculado completamente los gradientes, el modelo está listo para actualizar los pesos, manteniendo la precisión en cada etapa del entrenamiento.
El método de actualización de parámetros está implementado de una forma extremadamente sencilla, a la vez que elegante y fiable. Aquí no hay condiciones complejas ni cálculos adicionales; todo se reduce a transferir cuidadosamente el control a objetos internos que contienen los pesos entrenables directamente. Cada uno de ellos implementa su propio mecanismo de actualización basado en los gradientes acumulados y el esquema de optimización elegido.
bool CNeuronAGCN::updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *second) { if(!cEa.UpdateInputWeights()) return false; if(!cWe.UpdateInputWeights(cEa.AsObject())) return false; if(!cWx.UpdateInputWeights(NeuronOCL)) return false; if(!cEn.UpdateInputWeights(cWconcat_ex.AsObject())) return false; if(!cWpre.UpdateInputWeights(cApreX.AsObject())) return false; if(!cWadapt.UpdateInputWeights(cAadaptX.AsObject())) return false; //--- return true; }
Cada método se llama transmitiéndole el objeto de entrada necesario obtenido durante la pasada directa, lo que permite mantener la corrección de los cálculos bajo las condiciones de la topología de grafos.
Este enfoque hace que la estructura del código no solo sea legible, sino también fácilmente escalable.
Para mayor comodidad del lector y para garantizar una total transparencia técnica, el código fuente completo de esta clase, incluidas las definiciones de todos los métodos, se ofrece en el archivo adjunto.
La célula GinAR
Estamos entrando en la fase final: la creación de una célula computacional GinAR completa, que combina todos los componentes previamente comentados en una estructura única y coherente. Precisamente aquí, en la clase CNeuronGinARCell, convergen los mecanismos de atención por interpolación, la convolución de grafos adaptativa y los elementos de control de memoria, formando una arquitectura flexible, pero estrictamente estructurada.
class CNeuronGinARCell : public CNeuronBaseOCL { protected: CNeuronInterpolationAttention cX_IA; CNeuronAGCN cX_AGCN; CNeuronAGCN cForgetGate; CNeuronAGCN cResetGate; CNeuronBaseOCL cContext; CBufferFloat bTemp; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *second) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override; public: CNeuronGinARCell(void) { activation = None;} ~CNeuronGinARCell(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint dimension, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(const int file_handle) override; virtual bool Load(const int file_handle) override; //--- virtual int Type(void) override const { return defNeuronGinARCell; } virtual void TrainMode(bool flag) override; virtual void SetOpenCL(COpenCLMy *obj); //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau); virtual void SetActivationFunction(ENUM_ACTIVATION value) override {}; };
En la estructura anterior del nuevo objeto, se puede observar que CNeuronGinARCell hereda su interfaz básica de CNeuronBaseOCL. Esto garantiza la compatibilidad con otras capas y módulos del framework. No obstante, a diferencia de los bloques simples, aquí se agrupan varios componentes funcionales a la vez:
- cX_IA es un bloque de atención por interpolación responsable de suavizar y ajustar las series temporales de entrada;
- cX_AGCN — el módulo principal de la convolución adaptativa de grafos, que forma una representación generalizada de las características de entrada considerando sus conexiones;
- cForgetGate y cResetGate — dos subsistemas de grafos adicionales que implementan mecanismos de olvido y reinicio similares a los de las celdas GRU;
- cContext — un búfer interno que almacena el estado acumulado de la memoria oculta;
- bTemp — un búfer temporal que se utiliza en cálculos intermedios.
Esta clase implementa el ciclo de trabajo estándar de la capa neuronal. El método Init realiza la inicialización paso a paso de todos los componentes de la celda CNeuronGinARCell. Aquí se configuran los parámetros principales del modelo: el número de salidas, la dimensionalidad del espacio de características, el tipo de optimización y el tamaño del lote.
bool CNeuronGinARCell::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint dimension, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_count * dimension, optimization_type, batch)) return false;
A continuación, los bloques internos de la celda se inicializan uno por uno. Primero, cX_IA, que se encarga de la atención por interpolación.
int index = 0; if(!cX_IA.Init(0, index, OpenCL, units_count, dimension, optimization, iBatch)) return false;
A continuación, se inicializa el módulo principal de convolución de grafos cX_AGCN, seguido de las puertas de control cForgetGate y cResetGate. Cada uno de ellos recibe su propio índice único.
index++; if(!cX_AGCN.Init(0, index, OpenCL, units_count, dimension, optimization, iBatch)) return false; index++; if(!cForgetGate.Init(0, index, OpenCL, units_count, dimension, optimization, iBatch)) return false; index++; if(!cResetGate.Init(0, index, OpenCL, units_count, dimension, optimization, iBatch)) return false;
Un elemento independiente inicializa el módulo cContext, que actúa como medio de almacenamiento para el estado interno. Su salida se rellena explícitamente con ceros para evitar ruido aleatorio al arrancar, mientras que la función de activación está desactivada, lo que permite utilizarla como una memoria limpia.
index++; if(!cContext.Init(0, index, OpenCL, units_count * dimension, optimization, iBatch)) return false; if(!cContext.getPrevOutput().Fill(0)) return false; cContext.SetActivationFunction(None); bTemp.BufferFree(); if(!bTemp.BufferInit(units_count * units_count, 0) || !bTemp.BufferCreate(OpenCL)) return false; //--- return true; }
Finalmente, se crea el búfer temporal bTemp, necesario para realizar operaciones intermedias. El tamaño del búfer se selecciona de forma cuadrática como units_count * units_count. En este caso, si el búfer ya se ha creado previamente, se libera en primer lugar, lo cual garantiza un funcionamiento correcto y un ahorro de recursos.
Una vez completadas todas las operaciones, el módulo finaliza tras retornar el resultado lógico al programa que realiza la llamada.
El algoritmo de pasada directa del método, integrado en el método feedForward, implementa la lógica clave de la operación de la celda CNeuronGinARCell, combinando mecanismos de atención, convoluciones de grafos y gestión de memoria a través de puertas de olvido y reinicio.
bool CNeuronGinARCell::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!cX_IA.FeedForward(NeuronOCL)) return false;
En la primera fase, se activa el módulo cX_IA, que realiza una transformación por interpolación de los datos de origen de NeuronOCL. La representación resultante se usa como base para tres bloques paralelos: el módulo convolucional de grafos principal cX_AGCN, así como los módulos cForgetGate y cResetGate, que se encargan de generar las máscaras de control.
if(!cX_AGCN.FeedForward(cX_IA.AsObject(), SecondInput)) return false; if(!cForgetGate.FeedForward(cX_IA.AsObject(), SecondInput)) return false; if(!cResetGate.FeedForward(cX_IA.AsObject(), SecondInput)) return false; if(!Activation(cForgetGate.getOutput(), cForgetGate.getOutput(), GELU)) return false; if(!Activation(cResetGate.getOutput(), cResetGate.getOutput(), GELU)) return false;
Cada uno de estos tres bloques admite además una segunda entrada, SecondInput, que contiene información de grafos o estructural: una matriz de adyacencia o datos sobre los nodos vecinos. Tras atravesar las capas, las salidas cForgetGate y cResetGate se activan mediante la función GELU, lo que permite crear máscaras más suaves y flexibles que reducen el impacto de las transiciones bruscas y los valores negativos.
A continuación, se actualiza el contexto interno. Cabe destacar que la pasada directa sobrescribe inmediatamente los valores de contexto que necesitamos para las operaciones de pasada inversa. Para evitar esto, utilizamos un carrusel de búferes: llamamos al método SwapOutputs, que intercambia los punteros de los búferes Output y PrevOutput.
//--- Context if(!cContext.SwapOutputs()) return false; if(!GateElementMult(cContext.getPrevOutput(), cX_AGCN.getOutput(), cForgetGate.getOutput(), cContext.getOutput())) return false;
A continuación, se forma un nuevo vector de contexto mediante la multiplicación elemento a elemento del estado anterior cContext y los resultados cX_AGCN, ponderados por la máscara de olvido cForgetGate. Esto permite controlar de forma flexible qué elementos de memoria deben conservarse y cuáles deben borrarse.
El nuevo vector de contexto se pasa a través de una función de activación ELU adaptada para manejar valores negativos, lo cual mejora la estabilidad de los gradientes.
//--- Output if(!Activation(cContext.getOutput(), cContext.getPrevOutput(), ELU)) return false; if(!GateElementMult(cContext.getPrevOutput(), cX_IA.getOutput(), cResetGate.getOutput(), Output)) return false; //--- return true; }
Finalmente, la salida final del bloque en el búfer output se forma mediante la multiplicación elemento a elemento del contexto actualizado, el tensor de resultado cX_IA y la máscara cResetGate. Este paso combina la memoria a largo plazo, el estado de entrada actual y una máscara de reinicio encargada de transferir las características relevantes a la salida.
Si se produce un error o fallo en cualquier etapa de la operación, el método devuelve false. Si todas las operaciones se completan con éxito, true, lo que confirma que los datos se han transmitido correctamente a través de toda la estructura celular.
El algoritmo de propagación inversa no es tan simple como podría parecer a primera vista. Su principal dificultad radica en la necesidad de sumar y normalizar cuidadosamente los gradientes de múltiples fuentes, ya que los datos de origen de ambos troncos se utilizan en varios lugares a la vez: en el flujo principal (cX_AGCN) y en rutas auxiliares a través de las puertas de control cForgetGate y cResetGate. Por lo tanto, cada uno de estos consumidores genera su propia contribución al gradiente final, y todos deben combinarse correctamente.
bool CNeuronGinARCell::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -1) { if(!NeuronOCL || !SecondInput || !SecondGradient) return false; //--- Output if(!GateElementMultGrad(cContext.getPrevOutput(), cContext.getGradient(), cX_IA.getOutput(), cX_IA.getPrevOutput(), cResetGate.getOutput(), cResetGate.getGradient(), Gradient, ELU, cX_IA.Activation(), GELU)) return false;
Primero, el gradiente de la salida de la celda final se distribuye entre el contexto cContext, el resultado de la atención por interpolación cX_IA y la puerta de reinicio cResetGate. En este caso, se consideran todas las derivadas con respecto a las funciones de activación correspondientes.
A continuación, se restablece el gradiente del estado de memoria interna cContext. Como se forma a partir del resultado de la multiplicación de la salida del módulo convolucional principal cX_AGCN y la máscara cForgetGate, se utiliza una operación de propagación inversa similar. Aquí ya intervienen otras funciones de activación y otros gradientes, y el resultado se acumula en el objeto cContext.
//--- Context if(!GateElementMultGrad(cContext.getOutput(), cContext.getPrevOutput(), cX_AGCN.getOutput(), cX_AGCN.getGradient(), cForgetGate.getOutput(), cForgetGate.getGradient(), cContext.getGradient(), None, cX_AGCN.Activation(), GELU)) return false;
Luego llega uno de los momentos más cruciales: pasar los gradientes de vuelta al módulo de atención por interpolación cX_IA. Este era un predecesor común para tres ramas, por lo que requería tres llamadas independientes a CalcHiddenGradients, una para cada ruta: la principal (cX_AGCN), la puerta de olvido (cForgetGate) y la puerta de reinicio (cResetGate).
//--- Gradient to Interposition Attention if(!cX_IA.CalcHiddenGradients(cX_AGCN.AsObject(), SecondInput, SecondGradient, SecondActivation) || !SumAndNormilize(cX_IA.getGradient(), cX_IA.getPrevOutput(), cX_IA.getPrevOutput(), cForgetGate.GetWindow(), false, 0, 0, 0, 1)) return false; if(!cX_IA.CalcHiddenGradients(cForgetGate.AsObject(), SecondInput, GetPointer(bTemp), SecondActivation) || !SumAndNormilize(cX_IA.getGradient(), cX_IA.getPrevOutput(), cX_IA.getPrevOutput(), cForgetGate.GetWindow(), false, 0, 0, 0, 1) || !SumAndNormilize(SecondGradient, GetPointer(bTemp), SecondGradient, cForgetGate.GetUnits(), false, 0, 0, 0, 1)) return false; if(!cX_IA.CalcHiddenGradients(cResetGate.AsObject(), SecondInput, GetPointer(bTemp), SecondActivation) || !SumAndNormilize(cX_IA.getGradient(), cX_IA.getPrevOutput(), cX_IA.getPrevOutput(), cForgetGate.GetWindow(), false, 0, 0, 0, 1) || !SumAndNormilize(SecondGradient, GetPointer(bTemp), SecondGradient, cForgetGate.GetUnits(), false, 0, 0, 0, 1)) return false;
Tras cada llamada, el resultado se suma en el búfer bTemp y se devuelve cuidadosamente a SecondGradient, lo cual garantiza que todas las contribuciones se acumulen en un único flujo de salida.
Finalmente, se llama al cálculo del gradiente oculto en el objeto de datos de origen NeuronOCL vinculado con cX_IA. Esto completa la cadena de propagación de errores y garantiza que toda la información sobre cómo los cambios en las entradas afectan a la salida se propague con precisión hasta el inicio del modelo.
//--- if(!NeuronOCL.CalcHiddenGradients(cX_IA.AsObject())) return false; //--- return true; }
De este modo, el método implementa un mecanismo consistente y ponderado para la agregación y transmisión de gradientes en todas las direcciones. Esto garantiza el entrenamiento correcto de una estructura multicomponente compleja como GinAR, donde cada salida influye en varias ramas de computación a la vez.
El código completo de esta clase y todos sus métodos se ofrece en el archivo adjunto para su aprendizaje por cuenta propia. Si es necesario, puede rastrear fácilmente la cadena de cálculo, la estructura de dependencia entre los componentes y la lógica del flujo de datos en todas las etapas, desde la pasada directa hasta la propagación inversa y la actualización de los pesos.
Hoy hemos realizado una cantidad significativa de trabajo: paso a paso, hemos analizado la arquitectura, los métodos y la lógica interna de los componentes clave, sentando una base sólida para los pasos futuros. Le sugiero que se tome un breve descanso, recupere el aliento y deje que sus pensamientos se calmen. En el próximo artículo, llevaremos lo que hemos comenzado a su conclusión lógica: pasaremos de la teoría a la práctica y evaluaremos la efectividad en el mundo real de las soluciones implementadas usando datos de mercado.
Conclusión
En este artículo, hemos realizado un análisis en profundidad de la arquitectura de los componentes clave del framework GinAR. Hemos abarcado el proceso de inicialización de los componentes, la implementación de la pasada directa y hemos detallado los algoritmos de propagación inversa paso a paso. Hemos prestado especial atención al mecanismo de interacción entre los módulos de atención, las convoluciones de grafos y las puertas de control, lo que ha permitido crear una estructura flexible y expresiva.
Esta modularidad no solo simplifica la escalabilidad, sino que también abre el camino a la integración de bloques de control más complejos, ya sea en el contexto del aprendizaje por refuerzo o para tareas de predicción con dependencias variables.
Enlaces
- GinAR: An End-To-End Multivariate Time Series Forecasting Model Suitable for Variable Missing
- Otros artículos de la serie
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/18892
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Utilizando redes neuronales en MetaTrader
Introducción a MQL5 (Parte 24): Creación de un asesor experto (EA) que opere a partir de objetos del gráfico
Particularidades del trabajo con números del tipo double en MQL4
Automatización de estrategias de trading en MQL5 (Parte 18): Estrategia de scalping «Trend Bounce» con envolventes: Ejecución de operaciones y gestión del riesgo (Parte II)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso