Redes neuronales en el trading: Extracción eficaz de características para una clasificación precisa (Implementación de objetos)
Introducción
En el artículo anterior conocimos los aspectos teóricos del framework Mantis: un modelo fundacional para la clasificación de series temporales, sin la pesadez de los algoritmos de regresión y, aun así, sin rezagarse en cuanto a la precisión. El modelo convierte los datos brutos de la secuencia analizada en señales claras con niveles de confianza bien definidos, lo que aporta una nueva calidad al análisis.
Imagine un modelo clásico de predicción: aprende a minimizar el error de predicción, pero a menudo fracasa ante los picos repentinos de volatilidad. Estos algoritmos derivan detrás del mercado. Es difícil adaptarlos. Son propensos al sobreajuste y no ofrecen evaluaciones transparentes de la fiabilidad. Necesitamos algo más que una simple predicción del valor futuro. Debemos reconocer el régimen del mercado en tiempo real, cuando el precio se mueve según un escenario típico o anómalo. Es precisamente aquí donde Mantis nace como un modelo entrenado no tanto para la regresión como para la comprensión de patrones.
Mantis se basa en la idea de tokenizar una serie temporal tomada de los Transformers visuales. En lugar de analizar decenas de miles de ticks o segundos, la serie se divide en un número estrictamente definido de parches. Gracias a ello, las series temporales de distintos tamaños se procesan con el mismo número de pasos, lo que evita tener que adaptar la arquitectura a diferentes escalas. Después se produce la magia de la atención híbrida: en una ventana de observación, el modelo analiza los microcambios utilizando convoluciones y pooling, mientras que en la otra capta las tendencias a largo plazo mediante un mecanismo de atención global. La combinación de estas dos perspectivas permite al modelo percibir al mismo tiempo las oscilaciones más sutiles y captar el vector general del movimiento.
El ingrediente secreto clave del modelo es el preentrenamiento contrastivo. Imagine dos variantes ligeramente distorsionadas de un mismo fragmento de la evolución del precio. Mantis aprende a acercar sus embeddings en el espacio, como si comprimiera varias franjas de luz en un solo rayo, y al mismo tiempo a repeler segmentos totalmente diferentes, como un electrón y un positrón. Este contraste proporciona al modelo una percepción estable del patrón, lo que resulta especialmente valioso ante cambios en la amplitud, pequeños desplazamientos temporales o ruido atípico.
El proceso concluye con el escalado por temperatura: la calibración final de las salidas. Al operador no le interesa solo saber si hay «reversión/no reversión», sino el grado de confianza. Mantis puede emitir no valores abstractos, sino probabilidades a posteriori verosímiles. La práctica demuestra que, si el modelo afirma que hay un «80 % de probabilidad de reversión», en la realidad aproximadamente 80 de cada 100 señales de ese tipo resultarán acertadas.
El verdadero valor de Mantis se revela cuando recibe información multicanal. El RSI que se mueve al compás, los volúmenes, las medias móviles y las correlaciones entre pares de divisas conforman un panorama complejo. La concatenación directa de estas señales provoca un crecimiento explosivo del número de parámetros, mientras que el procesamiento por separado hace que se pierdan las relaciones cruzadas. La solución de Mantis consiste en adaptadores ligeros que comprimen las interacciones entre canales y dejan solo lo esencial: la fuerza de la relación entre los indicadores. Así, el modelo ahorra memoria, mientras que el operador ahorra tiempo de configuración.
Para comprender mejor la arquitectura de Mantis, vamos a recorrer sus etapas. La primera etapa es la convolución inicial: la serie temporal de d canales pasa por una capa convolucional con 256 salidas, formando una representación densa. A continuación, el tensor se divide en partes iguales y el mean-pooling por canal convierte estos datos en 32 tokens, cada uno de los cuales contiene información local.
Al mismo tiempo, se construye un flujo diferencial: las diferencias de primer orden de los datos originales aumentan la sensibilidad a la dinámica a corto plazo. Los datos obtenidos siguen el mismo proceso de patching.
Los flujos tercero y cuarto, de carácter estadístico, recopilan la media y la desviación estándar de los datos originales dentro de las mismas 32 ventanas, transmitiendo el fondo general de volatilidad y nivel.
Estos cuatro flujos pasan por proyectores lineales individuales para igualar la dimensionalidad y, a continuación, se combinan y se proyectan en tokens globales de tamaño especificado.
A la secuencia de tokens que codifican la secuencia analizada se le añade un token de clase y una codificación posicional sinusoidal, para que el modelo no pierda información sobre el orden. A continuación, los datos se introducen en el Transformer: 6 capas, atención multicabeza con 8 cabezas, normalización, una FFN de dos niveles con GELU y Dropout del 10 % durante la fase de entrenamiento.
El modelo se entrenó en dos fases. En la primera, de carácter contrastivo, se usó un corpus combinado de diez conjuntos de datos públicos: más de 7 millones de series temporales, 100 épocas y un batch de 2048 con 4 NVIDIA Tesla V100. Esto recuerda a una fase de entrenamiento intensivo. En la segunda fase ya se añadió la cabeza de clasificación y se aplicó el escalado por temperatura a las salidas. Este enfoque en dos fases permite alcanzar una combinación poco habitual en las redes neuronales: una alta precisión y una interpretabilidad fiable.
Por último, sobre la flexibilidad. Para hacer frente a tareas de distinta escala y adaptar los modelos a distintas restricciones, en función del presupuesto y de la estructura de los datos, los autores del framework Mantis proponen utilizar cinco tipos de adaptadores:
- El PCA y Truncated SVD: métodos clásicos de reducción lineal de dimensionalidad, probados por el tiempo;
- Random Projection: método rápido y sencillo, pero potente, para reducir el vector de características;
- Variance-Based Selector: selecciona únicamente los canales más informativos según la varianza;
- Differentiable Linear Combiner (LComb): un adaptador entrenable que se ajusta a una tarea concreta junto con el modelo principal.
Estos adaptadores permiten encontrar un equilibrio entre velocidad y precisión, ahorrar recursos computacionales y, al mismo tiempo, no perder conexiones intercanal críticas.
En definitiva, Mantis no es solo un modelo, sino todo un complejo de ingeniería para el análisis profundo de series temporales. Su fuerza no reside en las palabras grandilocuentes, sino en una ejecución minuciosa: atención esmerada a cada parche, una combinación cuidadosa de los contextos local y global, una calibración rigurosa y una compresión prudente de los canales. Esta alianza entre la analítica y la inteligencia de máquina permite no adivinar, sino actuar con seguridad, apoyándose en la estadística y en principios algorítmicos probados. En una época en la que la velocidad y la precisión lo deciden todo, Mantis se convierte en la herramienta que ayuda a mantener el equilibrio sobre la fina cuerda floja del mercado.
A continuación se muestra una visualización del autor del framework Mantis:

En la parte práctica del artículo anterior implementamos un componente base para el procesamiento de series temporales: el módulo CNeuronConcatDiff. Este objeto integró el algoritmo de cálculo de la primera diferencia y el mecanismo de concatenación de tensores procedentes de distintos flujos de información. Con él comenzó la implementación práctica del núcleo del framework Mantis.
Hoy continuaremos con la construcción de esta arquitectura, pasando a los siguientes bloques importantes del modelo neuronal. En la fase de planificación, en el artículo anterior, acordamos la necesidad de incorporar la codificación temporal en el flujo de datos. Esta decisión se tomó con el objetivo de mejorar la interpretabilidad y tener en cuenta la estructura posicional de los datos originales, sin perder por ello rendimiento. Por supuesto, no vamos a reinventar la rueda: por suerte, ya contamos con un objeto de eficacia probada: CMamba4CastEmbedding. Lo creamos en el marco del desarrollo del framework Mamba4Cast, y ahora resulta ideal para llevar a cabo esta tarea.
El siguiente paso lógico es organizar el patching de la serie temporal. Es precisamente en esta etapa cuando el modelo deja de estar vinculado a la longitud de la secuencia original y adquiere la capacidad de operar con un número fijo de parches, independientemente de la escala de los datos. Este es, sin exagerar, uno de los módulos más importantes de la arquitectura del clasificador, ya que de su implementación depende la estabilidad de todo el procesamiento posterior. Empezaremos por ahí el trabajo de hoy: paso a paso, crearemos un mecanismo que divida el flujo de información original en parches compactos e informativos.
Objeto de segmentación
Los autores del framework original Mantis abordaron la tarea de segmentación de series temporales desde un punto de vista inesperado, pero elegante. En lugar de limitarse a dividir la secuencia en segmentos iguales, propusieron transformar previamente los datos mediante una operación de convolución. Y no se trata simplemente de aplicar una convolución solo por cumplir, sino de utilizarla para convertir una serie temporal unidimensional en un tensor multicanal. Esta idea, a primera vista, puede parecer innecesaria, pero en la práctica ofrece una ventaja considerable: gracias a la convolución con una ventana pequeña, el modelo empieza a percibir las fluctuaciones locales de la señal y a captar matices importantes del comportamiento del mercado que, de otro modo, podrían perderse entre la tendencia general.
Tras esta transformación, obtenemos un tensor multicanal en el que cada canal representa una proyección independiente de la serie original; por así decirlo, una «mini-representación» de la señal desde su propio punto de vista. A continuación, este tensor se divide uniformemente en un número fijo de fragmentos (patches). Estos son precisamente los parches: los bloques de construcción con los que trabajará posteriormente el modelo. Y aquí es donde entra en juego la siguiente etapa: la agregación de información dentro de cada segmento.
Los autores de Mantis propusieron utilizar la operación clásica de promediado por canal, conocida como mean-pooling. Es sencilla, intuitiva y ofrece una buena representación suavizada del segmento. A diferencia del max-pooling, en el que solo se conserva el valor extremo, el mean-pooling conserva el contexto, lo que permite que cada elemento de la serie temporal contribuya al token final. Esto resulta especialmente útil cuando cada parche abarca una parte bastante grande de los datos; en esos casos, hay que tener en cuenta todo el volumen de información, y no solo los picos y los valles. Los tokens obtenidos de este modo constituyen una representación compacta e informativa de la estructura temporal de los datos originales, adecuada para su posterior análisis por parte del Transformer.
Sin embargo, como muestra la práctica, el mean-pooling puede ser demasiado «educado»: promedia no solo la información, sino también las posibles anomalías o los cambios bruscos en la estructura de la señal. Para un modelo fundacional, esto puede ser un compromiso aceptable: sigue siendo sencillo, rápido y generalizable. Pero decidimos ir un poco más allá. En nuestra implementación, hemos sustituido el mean-pooling estándar por una combinación de convolución por canal seguida de max-pooling.
¿Qué aporta esto en la práctica? Primero, la convolución extrae características dentro de cada canal, proporcionando un filtrado inicial y destacando los cambios locales. A continuación, el max-pooling destaca las manifestaciones más marcadas de estas características. Este enfoque hace que el modelo esté más afinado para la dinámica de la serie temporal. En lugar de promediar todo indiscriminadamente, detecta las características clave, lo que le permite reaccionar con mayor rapidez y precisión ante movimientos bruscos. Esta característica cobra una importancia fundamental en el contexto del trading de alta frecuencia o al operar con activos volátiles.
Precisamente gracias a esta modificación del patching, nuestro clasificador adquiere la capacidad de adaptarse a la forma y la estructura de cada serie temporal concreta. Ya no se limita a dividir los datos en segmentos; los analiza, extrae información, los filtra y los evalúa. En un sistema de este tipo, un parche ya no es una simple porción de la señal, sino un token completo y repleto de información que contiene tanto el contexto como la forma de la señal.
El algoritmo propuesto se ha implementado en la clase CNeuronMantisPatching. Se trata de un módulo especializado encargado de segmentar la serie temporal en parches estructurados y de generar una representación de alto nivel adecuada para su posterior procesamiento. Aquí comienza la verdadera comprensión de los datos: a partir de un simple flujo unidimensional se extraen patrones locales que se transforman en descripciones vectoriales densas.
La clase CNeuronMantisPatching está diseñada como un pipeline computacional de varias etapas. Contiene varios objetos internos, cada uno de los cuales realiza una tarea muy específica. A continuación se muestra la estructura de la clase.
class CNeuronMantisPatching : public CNeuronTransposeOCL { protected: CNeuronTransposeOCL cToVarSeq; CNeuronConvOCL cProjecting; CNeuronTransposeVRCOCL cToVarProjSeq; CNeuronConvOCL cPatchingProj; CNeuronProofOCL cProof; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronMantisPatching(void) {}; ~CNeuronMantisPatching(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint count, uint patchs, uint variables, uint embedding_size, uint patch_filters, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual int Type(void) override const { return defNeuronMantisPatching; } //--- virtual void SetOpenCL(COpenCLMy *obj) override; virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; };
Todos los objetos internos se declaran estáticamente, lo que permite dejar vacíos el constructor y el destructor de la clase. Su configuración se lleva a cabo en el método Init, cuyos parámetros nos proporcionan una serie de constantes que permiten determinar de forma inequívoca la arquitectura del objeto que se está creando.
bool CNeuronMantisPatching::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint count, uint patchs, uint variables, uint embedding_size, uint patch_filters, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronTransposeOCL::Init(numOutputs, myIndex, open_cl, variables * embedding_size, patchs, optimization_type, batch)) return false;
Cabe señalar que, como entrada del objeto CNeuronMantisPatching, esperamos recibir una matriz de valores de una serie temporal multicanal, en la que cada columna corresponde a un canal independiente. Además, cada canal es una secuencia de mediciones numéricas en el tiempo. En otras palabras, se trata de una matriz de dimensión [T × C], donde T es el número de pasos temporales y C es el número de canales (variables).
Para organizar el procesamiento independiente de los distintos canales dentro del módulo, se utilizan objetos de transposición de datos. Esto es necesario para garantizar un procesamiento separado por canales y ejes temporales; resulta difícil lograr este aislamiento sin mover explícitamente los ejes. Además, la propia clase CNeuronMantisPatching hereda de CNeuronTransposeOCL, lo que permite realizar la transposición inversa por medio de la clase padre una vez ejecutadas todas las transformaciones, devolviendo el tensor de salida a la forma esperada.
El primer paso del método de inicialización consiste en llamar al método homónimo de la clase padre CNeuronTransposeOCL. Es este método el que prepara la estructura básica y lleva a cabo la inicialización previa de los parámetros. Este paso es imprescindible para que todos los mecanismos heredados funcionen correctamente.
Una vez inicializada correctamente la clase padre, comienza la configuración de los componentes internos. El primero de ellos que configuramos es el objeto cToVarSeq: se trata de una capa de transposición de matrices que facilita el acceso a las secuencias temporales individuales de cada canal. Su objetivo es reorientar los datos de entrada de tal forma que cada canal se convierta en una serie temporal independiente, apta para una posterior convolución local.
int index = 0; if(!cToVarSeq.Init(0, index, OpenCL, count, variables, optimization, iBatch)) return false;
A continuación, inicializamos el objeto de codificación local cProjecting. Se trata de una capa convolucional que actúa como un «observador atento», recorriendo cada canal de la serie temporal con una ventana fija de anchura 3 y paso 1. Su objetivo es identificar patrones locales a corto plazo y representarlos en un espacio de dimensión fija embedding_size.
index++; if(!cProjecting.Init(0, index, OpenCL, 3, 1, embedding_size, count - 2, variables, optimization, iBatch)) return false; cProjecting.SetActivationFunction(SoftPlus);
Es importante destacar que el procesamiento sigue siendo estrictamente por canales: cada canal se analiza de forma aislada, lo que permite que el modelo se adapte a las características individuales de las señales. Es más, a cada canal le corresponde su propio conjunto único de filtros convolucionales, lo cual confiere al proceso una flexibilidad y precisión especiales.
El resultado de esta operación es un tensor de dimensión [C * T' * D], donde:
- C — número de canales (características),
- T' — longitud del eje temporal, teniendo en cuenta el recorte tras la convolución con una ventana de 3,
- D — dimensión de la nueva representación tras la codificación.
Este tensor es una forma localmente comprimida y ya parcialmente estructurada de los datos originales, que servirá de base para la posterior segmentación y agregación. De este modo, obtenemos una especie de retrato condensado de la dinámica a corto plazo dentro de cada característica, algo que los métodos tradicionales de análisis de series temporales rara vez son capaces de captar sin recurrir a transformaciones engorrosas y que consumen muchos recursos.
A continuación viene la fase de segmentación. Y aquí es importante tener en cuenta un matiz. En nuestro contexto, la operación de segmentación en sí misma supone dividir los datos a lo largo del eje temporal, es decir, descomponer una secuencia en segmentos de longitud fija. Sin embargo, en el paso anterior, el resultado de la convolución se presenta en forma de tensor, en el que el eje temporal constituye la segunda dimensión. Para facilitar el procesamiento posterior y alinearlo con la arquitectura de las capas siguientes, debemos intercambiar los ejes.
Por eso utilizamos un objeto de transposición de un tensor tridimensional, llevando el eje temporal al primer plano. Como resultado, obtenemos un tensor con una nueva estructura [C * D * T'].
index++; if(!cToVarProjSeq.Init(0, index, OpenCL, variables, count-2, embedding_size, optimization, iBatch)) return false;
Esta forma de representar los datos resulta práctica y lógica: ahora podemos operar directamente con la secuencia temporal, dividiéndola en parches iguales, cada uno de los cuales se considerará como un segmento semántico independiente. Esto se ajusta a nuestra intención inicial: pasar del análisis de puntos al análisis de patrones.
Es precisamente en esta nueva forma como el tensor pasa a la siguiente etapa, donde se lleva a cabo la operación principal de segmentación y agregación de la información. El objetivo principal es dividir la secuencia temporal en un número fijo de segmentos y extraer de cada uno de ellos una representación informativa. Pero, dado que inicialmente solo se nos ha indicado el número de segmentos (parches) necesarios, lo primero que hay que hacer es calcular el tamaño de un segmento: cuántos pasos temporales entrarán en cada parche.
A continuación, inicializamos la capa convolucional cPatchingProj, que será la encargada de realizar la segmentación. Utiliza una ventana igual al tamaño del parche calculado y se desplaza por el eje temporal con el mismo paso, lo que garantiza la creación de segmentos que no se solapan.
index++; int patch_size = (int(count + patchs) - 3) / int(patchs); if(!cPatchingProj.Init(0, index, OpenCL, patch_size, patch_size, patch_filters, patchs, variables * embedding_size, optimization, iBatch)) return false; cPatchingProj.SetActivationFunction(SoftPlus);
Es importante señalar que, a diferencia del simple mean-pooling, aquí se aplica una operación convolucional completa con múltiples filtros (patch_filters). Esto permite que el modelo no se limite a promediar la información del segmento, sino que extraiga las características más relevantes de cada segmento, reaccionando ante los patrones locales característicos.
Tras pasar el tensor por la capa cPatchingProj, obtenemos un tensor cuatridimensional de dimensiones [C × D × P × F], donde:
- P — número de segmentos (parches),
- F — número de filtros aplicados a cada parche.
Este tensor contiene representaciones enriquecidas de cada segmento de cada canal para todos los filtros. Sin embargo, el procesamiento posterior requiere la agregación de estos datos. Es precisamente aquí donde se aplica la operación de max-pooling sobre la última dimensión, es decir, sobre el eje de los filtros F. Esta operación selecciona el valor máximo en cada dimensión y permite descartar los filtros redundantes o con activación débil. Después de esto, la dimensionalidad del tensor pasa a ser [C × D × P].
index++; if(!cProof.Init(0, index, OpenCL, patch_filters, patch_filters, patchs*variables*embedding_size, optimization, iBatch)) return false; //--- return true; }
La transformación final de los datos se lleva a cabo mediante la clase padre, que ya habíamos inicializado anteriormente. Por lo tanto, finalizamos la ejecución del método devolviendo al programa llamador el resultado lógico de las operaciones realizadas.
Cabe destacar que la arquitectura CNeuronMantisPatching se ha diseñado con un alto grado de modularidad. Cada capa interna trabaja con los canales de forma independiente, lo que hace que la estructura sea extremadamente escalable. Independientemente del número de canales que se analicen, la lógica de procesamiento sigue siendo la misma, ya que cada canal se percibe como un flujo de datos independiente.
Es más, esta estructura independiente permite paralelizar los cálculos de forma eficaz. Dado que todas las capas neuronales utilizadas están implementadas en OpenCL, aprovechan automáticamente la GPU disponible o cualquier otro dispositivo de cómputo compatible. Esto permite ejecutar simultáneamente cientos de hilos paralelos que procesan canales individuales sin una pérdida de tiempo apreciable.
Hemos explicado detalladamente la lógica principal del funcionamiento de la clase CNeuronMantisPatching y la interacción entre sus componentes internos al analizar la estructura del método de inicialización. Es precisamente en él donde se estructuran de forma secuencial todas las etapas clave del procesamiento de una serie temporal multicanal: desde la transposición inicial hasta la segmentación y la agregación de la información.
Para no sobrecargar el artículo con detalles innecesarios, hemos omitido deliberadamente la descripción de los métodos de pasada directa (feedForward) y de pasada inversa (calcInputGradients, updateInputWeights). En ellos se implementa una llamada estrictamente secuencial a los métodos homónimos de los objetos internos, que corresponden a la arquitectura del pipeline computacional construido.
El código fuente completo de esta clase, incluida la implementación de todos los métodos auxiliares, se encuentra en el archivo adjunto. Si lo desea, el lector puede consultarlo por su cuenta y profundizar en todos los detalles técnicos.
Módulo de atención
Según la lógica general del framework Mantis, la secuencia preprocesada de la serie temporal —ya en forma de tokens formados a partir de parches de canales individuales— se envía al módulo Transformer. Es precisamente aquí donde tiene lugar el procesamiento final de la información temporal y estructural: se añade un token de clase a la secuencia de tokens y se integra la codificación posicional.
El token de clase actúa como una especie de agregador de información. Durante su paso por las 6 capas del mecanismo de autoatención multicabeza (Self-Attention), este agrega información estructural sobre toda la secuencia. Es precisamente su valor a la salida del Transformer el que se interpreta como la representación de clase de la serie temporal analizada, ya se trate del reconocimiento del régimen del mercado, de la clasificación del estado de los indicadores o de la predicción del tipo de movimiento futuro.
En nuestra implementación, hemos introducido algunos ajustes en esta fase. En primer lugar, hemos eliminado la codificación posicional. El motivo es que la estructura temporal de la secuencia ya está explícitamente codificada en los propios tokens gracias a la codificación temporal incorporada, tomada de la arquitectura Mamba4Cast. Este enfoque permite codificar la posición absoluta y relativa de cada elemento dentro del canal sin recurrir a vectores posicionales sinusoidales o entrenables, como es habitual en los Transformers clásicos.
Este cambio ofrece dos ventajas a la vez. En primer lugar, hace que el modelo sea menos sensible a los desplazamientos y las distorsiones de la secuencia original. En segundo lugar, se elimina la competencia entre el código posicional rígido y el contexto temporal real, que ya está presente en los datos. En series temporales reales de mercado, esto permite aumentar la robustez del modelo y que el Transformer se centre en el contenido de la señal de entrada.
Además, en la implementación original de Mantis, a la salida de Transformer se utiliza exclusivamente el token de clase: un vector especial destinado a acumular información generalizada sobre la secuencia. Este token se añade previamente a la secuencia de entrada, pasa por todas las capas del Transformer y se extrae a la salida.
Sin embargo, en nuestra implementación hemos adoptado una solución alternativa. En lugar de la inserción tradicional del token de clase al principio de la secuencia y su posterior extracción, utilizamos el módulo de atención cruzada (Cross-Attention). Su particularidad radica en que el token de clase se pasa como consulta principal (query), mientras que los tokens de la secuencia original se pasan como contexto (keys y values). Esta configuración permite que el token de clase se centre en los elementos más significativos de la secuencia, minimizando el ruido y potenciando las relaciones relevantes.
Además, hemos aplicado una variante de atención cruzada con canales independientes, en la que cada canal se analiza de forma aislada. Esto permite evaluar por separado la contribución de cada canal a la atención. Este enfoque resulta especialmente útil al analizar señales multimodales o de distintos tipos, ya que permite evitar que un canal predomine sobre los demás y formar una representación agregada objetiva de toda la estructura de la serie temporal.
De este modo, el vector-contenedor final obtenido a la salida del bloque de atención cruzada ya contiene una representación generalizada y equilibrada de toda la estructura temporal, adecuada para la posterior clasificación o predicción.
Desde el punto de vista técnico, todo esto se ha implementado en la clase CNeuronMantisAttentionUnit, que hereda de CNeuronSoftMaxOCL. Esto significa que, como resultado, obtenemos directamente la probabilidad de que la secuencia analizada pertenezca a una u otra clase. A continuación se muestra la estructura del nuevo objeto.
class CNeuronMantisAttentionUnit : public CNeuronSoftMaxOCL { protected: CNeuronBaseOCL cClassToken[2]; CNeuronMVCrossAttentionMLKV cAttention; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronMantisAttentionUnit(void) {}; ~CNeuronMantisAttentionUnit(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint token_size, uint window, uint window_key, uint heads, uint units_count, uint layers, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual int Type(void) override const { return defNeuronMantisAttentionUnit; } //--- virtual void SetOpenCL(COpenCLMy *obj) override; virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; };
Dentro de la nueva clase vemos dos componentes principales:
- cClassToken[2]: MLP de dos capas para generar un token de clase entrenable;
- cAttention: el objeto de atención cruzada multicapa propiamente dicho.
Todos los objetos internos se declaran estáticamente, lo que permite dejar vacíos el constructor y el destructor de la clase. La inicialización de los objetos internos se lleva a cabo, como es habitual, en el método Init.
bool CNeuronMantisAttentionUnit::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint token_size, uint window, uint window_key, uint heads, uint units_count, uint layers, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronSoftMaxOCL::Init(numOutputs, myIndex, open_cl, token_size, optimization_type, batch)) return false;
Aquí, en primer lugar, llamamos al método homónimo de la clase padre, donde, como ya sabe, ya se ha organizado el proceso de inicialización de los objetos e interfaces heredados. Tras ejecutar correctamente las operaciones del método de la clase padre, pasamos a la inicialización de los objetos internos. En primer lugar, inicializamos la MLP que genera el token de clase entrenable. La primera capa siempre tiene un único elemento con un valor fijo, mientras que la segunda, mediante parámetros entrenables, genera un token de clase del tamaño necesario.
int index = 0; if(!cClassToken[0].Init(token_size, index, OpenCL, 1, optimization, iBatch)) return false; if(!cClassToken[0].getOutput().Fill(1)) return false; index++; if(!cClassToken[1].Init(0, index, OpenCL, token_size, optimization, iBatch)) return false; cClassToken[1].SetActivationFunction(SIGMOID);
Para obtener los elementos del token dentro de un rango de valores determinado, utilizamos una función de activación sigmoide en la última capa.
El siguiente paso consiste en inicializar el bloque de atención cruzada. Obtenemos del usuario todos los datos sobre su configuración a través de los parámetros de nuestro método de inicialización.
index++; if(!cAttention.Init(0, index, OpenCL, token_size, window_key, heads * variables, window, heads, 1, units_count, layers, 1, 1, variables, optimization, iBatch)) return false; //--- return true; }
Finalizamos la ejecución del método devolviendo el resultado lógico de las operaciones al programa que lo ha llamado.
El método de pasada directa feedForward resulta igualmente conciso. En los parámetros del método obtenemos un puntero al objeto de contexto de la secuencia que se está analizando y comprobamos inmediatamente su validez.
bool CNeuronMantisAttentionUnit::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false;
A continuación, tenemos que generar el token de clase. Sin embargo, esta operación solo se lleva a cabo durante el entrenamiento del modelo. Durante el funcionamiento, los parámetros del modelo no varían y, por lo tanto, el token de clase permanece fijo. No es necesario generarlo de nuevo en cada iteración.
//--- if(bTrain) { if(!cClassToken[1].FeedForward(cClassToken[0].AsObject())) return false; }
El token de clase generado y el contexto de la secuencia que se está analizando se introducen en el bloque de atención.
if(!cAttention.FeedForward(cClassToken[1].AsObject(), NeuronOCL.getOutput())) return false;
El resultado del bloque de atención —un token de clase enriquecido— se pasa a la entrada de la clase padre CNeuronSoftMaxOCL. Este paso transforma la salida del modelo en una interpretación probabilística. Como resultado, obtenemos una distribución de probabilidad por clases.
if(!CNeuronSoftMaxOCL::feedForward(cAttention.AsObject())) return false; //--- return true; }
Para terminar, cabe señalar que los métodos de pasada inversa (calcInputGradients y updateInputWeights) implementan una llamada secuencial a las funciones correspondientes de los componentes anidados. El lector puede estudiar por su cuenta su implementación para dominar por completo el proceso de entrenamiento y actualización de los pesos en el módulo CNeuronMantisAttentionUnit. El código completo de esta clase y de todos sus métodos se incluye en el archivo adjunto.
Hemos completado todo el trabajo previsto para hoy. En el próximo artículo analizaremos la arquitectura de los modelos y evaluaremos la eficacia de las soluciones implementadas utilizando datos históricos reales.
Conclusión
Ya hemos recorrido el proceso completo, desde la preparación inicial de los datos —el cálculo de las primeras diferencias, la concatenación de características y la codificación temporal basada en CMamba4CastEmbedding— hasta el procesamiento complejo de parches y la agregación inteligente mediante atención cruzada con ayuda de un token de clase. Cada paso de esta cadena —desde la transposición y las convoluciones locales hasta el max-pooling y la atención multicabeza— se ha diseñado minuciosamente para conservar una visión lo más completa posible de la dinámica temporal y, al mismo tiempo, optimizar los recursos computacionales, haciendo hincapié en el paralelismo de OpenCL.
El resultado ha sido una arquitectura modular en la que es fácil ajustar el número de canales, el tamaño de los parches, la dimensión del embedding y el número de cabezas de atención, sin modificar la lógica básica. Esto permite adaptar rápidamente el modelo a los más diversos escenarios financieros, desde el trading de alta frecuencia hasta el análisis de tendencias a largo plazo.
En la siguiente parte, presentaremos el esquema completo del modelo entrenable basado en los componentes descritos y mostraremos los resultados de su funcionamiento con datos históricos.
Enlaces
- Mantis: modelo fundacional ligero y calibrado para la clasificación sencilla de series temporales
- Otros artículos de la serie
Programas utilizados en el artículo
| # | Nombre | Tipo | Descripción |
|---|---|---|---|
| 1 | Research.mq5 | asesor experto | asesor experto para recopilar ejemplos |
| 2 | ResearchRealORL.mq5 | asesor experto | Asesor experto para la recopilación de ejemplos mediante el método Real-ORL |
| 3 | StudyContrast.mq5 | asesor experto | Asesor experto para el aprendizaje contrastivo del Encoder |
| 4 | Study.mq5 | asesor experto | Asesor experto para el entrenamiento offline de modelos |
| 5 | StudyOnline.mq5 | asesor experto | Asesor experto para el entrenamiento online de modelos |
| 6 | Test.mq5 | asesor experto | asesor experto para probar el modelo |
| 7 | Trajectory.mqh | Biblioteca de la clase | Estructura de descripción del estado del sistema y de la arquitectura de los modelos |
| 8 | NeuroNet.mqh | Biblioteca de la clase | Biblioteca de clases para crear una red neuronal |
| 9 | 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/18307
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
Automatización de estrategias de trading en MQL5 (Parte 24): Sistema de ruptura de la sesión de Londres con gestión del riesgo y stops dinámicos
Particularidades del trabajo con números del tipo double en MQL4
Características del Wizard MQL5 que debe conocer (Parte 76): Uso de los patrones del Awesome Oscillator y los canales Envelopes con aprendizaje supervisado
- 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