Redes neuronales en el trading: descomposición en lugar de escalado (SSCNN)
Introducción
La predicción de series temporales sigue siendo una de las áreas fundamentales del análisis de datos, el aprendizaje automático y la estadística. Es difícil exagerar su importancia: desde las finanzas y la meteorología hasta la infraestructura urbana y las telecomunicaciones, en todos aquellos ámbitos en los que la dinámica y la previsibilidad son fundamentales, las series temporales desempeñan un papel clave. Sin embargo, los modelos clásicos unidimensionales, como ARIMA o los métodos de suavizado exponencial, se muestran cada vez más ineficaces ante la complejidad de los sistemas abiertos y en constante evolución.
La situación empezó a cambiar radicalmente con la aparición de los métodos de aprendizaje profundo. Se produjo un avance especialmente notable tras la introducción de la arquitectura Transformer, capaz no solo de detectar dependencias complejas por pares en las secuencias, sino también de extraer sus representaciones multinivel. Son precisamente estas propiedades las que han convertido al Transformer en una de las tecnologías clave de la predicción moderna, estableciendo un nuevo estándar de precisión en los modelos.
En el contexto del rápido desarrollo de los grandes modelos de lenguaje (LLM), el aumento de la escala de las redes neuronales se ha convertido, en esencia, en la tendencia dominante. Hoy en día, la gran mayoría de los modelos avanzados cuentan con millones de parámetros y, en el caso de los LLM preentrenados, incluso miles de millones. Parecería que un aumento de escala de este tipo debería haber supuesto un salto cualitativo en los resultados. Sin embargo, contrariamente a lo esperado, las mejoras resultaron ser más bien moderadas: el aumento de la precisión (en términos de MSE y MAE) no supera el 30 %, mientras que el número de parámetros se multiplica por cientos e incluso miles en comparación con los modelos lineales simples. Es más, a partir del modelo PatchTST, el progreso comenzó a ralentizarse de forma acusada: los modelos posteriores solo muestran mejoras graduales e insignificantes.
Estas observaciones ponen en duda la propia dirección que está tomando este campo. Es evidente que la escala del modelo, por sí sola, no garantiza una alta calidad. En este contexto, cobra cada vez más relevancia la tendencia opuesta: reducir el número de parámetros sin perder capacidad de pronóstico.
Sin embargo, antes de proponer una solución alternativa, es importante comprender por qué los métodos actuales funcionan mal cuando se reduce el número de parámetros. En la mayoría de las arquitecturas modernas se utiliza el denominado patching / segmentación en parches: la división de los datos en fragmentos según dimensiones temporales o espaciales. En combinación con el mecanismo de atención, este enfoque permite, en efecto, captar dependencias complejas. Pero también tiene su lado negativo. Con la segmentación en parches (patching), se pierden los identificadores temporales (o espaciales), así como las relaciones entre las observaciones y, por lo tanto, desaparece precisamente esa estructura que el modelo debe captar.
Para compensar en parte estas pérdidas, los investigadores introducen una codificación adicional de las identidades y las posiciones temporales, ampliando así el espacio oculto de características. Pero cuantas más identidades de este tipo haya que conservar, mayor será la dimensionalidad del espacio y, con ella, el número de parámetros. Como resultado, el tamaño del modelo aumenta de forma exponencial. Esto, a su vez, aumenta el riesgo de sobreajuste, especialmente en los casos en que se dispone de una cantidad limitada de datos de entrenamiento, algo típico en las tareas de series temporales.
Si el objetivo es obtener un modelo que no solo sea eficaz, sino también eficiente en parámetros, es necesario replantearse el paradigma en sí: en lugar de recrear la estructura en un espacio oculto, es mejor conservar y aprovechar las regularidades de los datos desde el principio. Los últimos estudios demuestran que la descomposición de características (feature decomposition) puede mejorar considerablemente la precisión sin necesidad de recurrir a modelos gigantescos. Sin embargo, este enfoque también tiene sus limitaciones. En primer lugar, resulta poco aplicable a las tareas de predicción a largo plazo, especialmente cuando los datos muestran interrelaciones espaciotemporales complejas. Además, existe un aspecto analítico de la cuestión: ¿por qué y cómo funciona la descomposición, y qué ventajas tiene frente a la segmentación en parches (patching)?
En respuesta a estos problemas, los autores del artículo «Parsimony or Capability? Decomposition Delivers Both in Long-term Time Series Forecasting» proponen un nuevo enfoque: la arquitectura de red neuronal SSCNN (Selective Structured Components-based Neural Network). Este modelo combina precisión, eficiencia de parámetros y una base analítica sólida. A diferencia de las soluciones anteriores, la SSCNN ofrece por primera vez un análisis formal de las ventajas de la descomposición de características frente a la segmentación en parches (patching), justificándola tanto desde el punto de vista de la eficiencia como de la compacidad. Además, la descomposición básica se ve reforzada aquí por un mecanismo de selección: el modelo es capaz de identificar dependencias importantes a nivel de pasos temporales individuales, lo que mejora considerablemente la precisión en la reconstrucción de los componentes estructurales y, por consiguiente, la calidad global de la predicción.
Los resultados de las pruebas de SSCNN en tareas de referencia, realizadas por los autores del framework, muestran que el modelo supera de forma constante a los métodos existentes en cuanto a la calidad de la predicción y, al mismo tiempo, en el 99% de los casos requiere menos parámetros que los modelos PatchTST o iTransformer. Y lo que es aún más sorprendente: al resolver tareas de predicción a largo plazo, la SSCNN utiliza un 87% menos de parámetros que incluso el modelo ultracompacto DLinear.
La SSCNN supone un paso adelante: no por incrementar la potencia de cálculo, sino gracias al uso inteligente de la estructura de los datos y a una ingeniería precisa. Esta solución pone de manifiesto que el camino hacia una predicción de calidad no pasa por la cantidad, sino por la comprensión.
El algoritmo SSCNN
En el problema de predicción multivariante de series temporales, dadas las observaciones históricas X = {x1, …, xN} ∈ RN*Tin, donde N es el número de variables y Tin es la longitud de la secuencia analizada, es necesario predecir los valores en un intervalo futuro de longitud Tout, denotado como X̂ ∈ RN*Tout. Los datos de partida se someten a un preprocesamiento con el fin de predecir valores futuros aún desconocidos. Durante el proceso de predicción se genera una secuencia de representaciones intermedias que reflejan la dinámica interna de los datos.
La arquitectura SSCNN se organiza en dos ramas funcionalmente distintas. La rama superior se encarga del proceso de inferencia: extrae las componentes estructurales de la serie temporal junto con los residuos correspondientes. La rama inferior, por su parte, se centra en la extrapolación, es decir, en la predicción de la posible evolución de estos componentes a lo largo del tiempo.
Los componentes y los residuos obtenidos se concatenan en un único vector de gran dimensionalidad, que a continuación se introduce en la capa de regresión polinómica. Esta capa permite identificar relaciones complejas entre los elementos, conservando al mismo tiempo la organización estructural de los datos.
Los autores del framework SSCNN introducen un nuevo mecanismo de normalización — T-AttnNorm —, basado en la atención temporal. Permite identificar paso a paso las componentes principales de una serie temporal: la componente de largo plazo, la componente estacional y la componente de corto plazo. Cada uno de estos elementos se extrae individualmente para cada variable a lo largo del eje temporal. Al mismo tiempo, se genera un mapa de atención (selection map) que refleja la dinámica de la componente de interés.
Tras extraer una componente, el modelo la separa de la representación general de la serie, obteniendo así una señal residual en la que se acumula la información que no ha quedado incorporada a la estructura del elemento extraído. De este modo, a partir de la representación inicial de la serie temporal H ∈ RN*Tin*d se forman dos flujos: la componente estructural µ ∈ RN*Tin*d y el residuo R ∈ RN*Tin*d. El mapa de selección I ∈ RN*Tin*Tin controla la formación de cada componente.
![]()

Para garantizar la corrección de la normalización, las filas de la matriz I* se normalizan de modo que la suma de cada una sea igual a 1. La diferencia entre las componentes de largo plazo, estacional y de corto plazo radica precisamente en la forma de construir esta matriz.
Es importante señalar que el residuo obtenido en una etapa se envía a la entrada del siguiente bloque. Por ejemplo, para aislar el componente estacional, se utiliza como entrada el residuo restante tras extraer el componente de largo plazo.
Tras la descomposición de cada componente estructural y de la señal residual, ambos flujos se extrapolan a un horizonte temporal futuro mediante un mapeo lineal definido por la matriz E* ∈ RN*Tin*Tout. Al igual que el mapa de atención I*, la matriz E* también está normalizada por filas.

Como resultado, obtenemos los componentes pronosticados ̂µ* y R̂* ∈ RN*Tout*d.
La componente de largo plazo del modelo sirve para identificar y describir las tendencias estables en las series temporales. Para obtener una estimación precisa y no sesgada de este componente, los autores del framework calculan la media de los valores recopilados a lo largo de varios periodos. Este enfoque permite minimizar el impacto de las fluctuaciones estacionales y de corto plazo, que solo tienen un efecto local y pueden distorsionar la dirección general de la dinámica.
La implementación de los mecanismos de extracción y extrapolación del componente de largo plazo se expresa mediante matrices simples en las que todos los valores son iguales: cada celda de dichas matrices toma el valor 1/Tin. Esto significa que cada punto temporal del pasado contribuye por igual a la configuración del panorama de largo plazo. De este modo, ningún tramo de la serie recibe prioridad; al contrario, el modelo se basa en un comportamiento global y promediado.
Los autores del framework han excluido deliberadamente el mecanismo de atención (attention) del procesamiento del componente de largo plazo. La experiencia ha demostrado que, en los conjuntos de datos analizados, esto no mejora la precisión de los pronósticos. La atención puede resultar realmente útil cuando la distribución de una componente varía notablemente con el tiempo; en ese caso, ayuda a reducir el sesgo de la estimación. Sin embargo, en el caso de la componente de largo plazo, dicha distribución se mantiene estable a lo largo de todo el intervalo de entrada. Por lo tanto, el uso de atención en este contexto no está justificado y solo supone una complejidad innecesaria sin utilidad práctica.
La componente estacional del modelo está destinada a describir oscilaciones regulares que se repiten con una ciclicidad determinada. Su extracción se basa en la hipótesis de una longitud de ciclo estable, lo que facilita la identificación de patrones estacionales. Aquí se introduce la notación c para la longitud de un ciclo. En ese caso, τin indica el número de ciclos completos contenidos en la secuencia analizada (τin • c ≤ Tin), mientras que τout indica la cantidad mínima de ciclos necesaria para cubrir la secuencia de salida (τout • c ≥ Tout). Para facilitar los cálculos posteriores, se supone que la longitud de la entrada Tin es múltiplo de c.
Para obtener una estimación precisa y no sesgada de la componente estacional, los autores del framework SSCNN introducen una matriz de parámetros entrenable Wse ∈ Rτin*τin, en la que cada celda refleja la relación entre un par de ciclos. Esta matriz se normaliza por filas mediante SoftMax. De este modo, al calcular los pesos para el ciclo actual se tiene en cuenta la contribución de todos los ciclos anteriores, lo que permite captar las regularidades locales y globales.
El modelo crea un mapa de selección especial, una especie de filtro que permite extraer únicamente los valores correspondientes a fases idénticas de distintos ciclos. Es decir, se comparan, por ejemplo, solo los primeros días de cada mes, o solo los lunes de todas las semanas, dependiendo de la escala del ciclo. Esto se consigue teniendo en cuenta únicamente los elementos cuya diferencia entre índices sea múltiplo de c, y la pertenencia a un ciclo concreto se fija mediante una división entera.
Al pasar a la extrapolación, es decir, al pronosticar la componente estacional para puntos temporales futuros, se aplica una lógica similar. Se introduce la matriz Ŵse ∈ Rτout*τin, que refleja las relaciones entre los ciclos de las secuencias de salida y de entrada. También se normaliza y forma un mapa de selección para la extrapolación, lo que garantiza una prolongación lógica y coherente hacia el futuro de los patrones estacionales ya identificados.
Esta organización permite que el modelo tenga en cuenta con precisión las estructuras de datos recurrentes y las utilice como anclaje para las predicciones, lo que proporciona una gran estabilidad y precisión en los pronósticos estacionales.
La componente de corto plazo está destinada a detectar anomalías locales y efectos breves que no encajan en los patrones estacionales ni de largo plazo. A diferencia de la tendencia, se forma a partir de una ventana de observación limitada δ, que determina la profundidad temporal de la mirada hacia atrás. Estos datos mantienen su relevancia solo en el horizonte temporal más cercano y muestran distintos grados de correlación en función de los retardos, es decir, de la distancia entre los puntos de observación.
Precisamente por eso, la extracción de la componente de corto plazo se basa en el vector de parámetros wst ∈ Rδ, que permite ponderar de forma diferente los valores dentro de una ventana especificada. Estos pesos pasan por una normalización SoftMax, lo que permite destacar las observaciones más significativas y, al mismo tiempo, mantener la estabilidad numérica del modelo. De este modo, la atención se centra únicamente en los últimos valores de la serie temporal, lo que garantiza el foco en los cambios más cercanos al pronóstico.
Cuando se trata de pronosticar la componente de corto plazo, el enfoque depende de hasta dónde se extienda el horizonte temporal. Si la predicción abarca los pasos temporales más cercanos, se mantienen las correlaciones con los valores recientes y se utiliza un modelo de regresión basado en la matriz de parámetros wst ∈ Rδ*δ. Sin embargo, a medida que nos alejamos hacia horizontes más lejanos, se acumulan incertidumbres que reducen la fiabilidad de las predicciones. En estos casos, el modelo aplica el método de relleno con ceros (zero-padding), prescindiendo deliberadamente de los parámetros redundantes que podrían provocar sobreajuste.
De este modo, la componente de corto plazo sigue siendo compacta y flexible, captando con precisión las oscilaciones locales sin sobrecargar el modelo con cálculos y pesos innecesarios. Esta capacidad de adaptación resulta especialmente importante en contextos de datos inestables o ruidosos, en los que precisamente la dinámica de corto plazo desempeña un papel clave en la toma de decisiones.
La componente espacial abarca aquellos aspectos de las series temporales que no pueden describirse mediante las estructuras temporales identificadas previamente: la componente de largo plazo, la componente estacional y la componente de corto plazo. En otras palabras, refleja características que, aunque temporalmente irregulares, son espacialmente coherentes y se manifiestan de forma similar en varias series simultáneamente.
La extracción de esta componente se lleva a cabo mediante un mecanismo especial de normalización basado en la atención espacial: S-AttnNorm. A diferencia de la normalización temporal, que se aplica a lo largo del eje del tiempo, en este caso el procesamiento se realiza para cada corte temporal, a lo largo de la dimensión espacial. Es decir, en cada paso temporal, el modelo analiza el comportamiento de todas las series al mismo tiempo, identificando sus características comunes.
Los cálculos se organizan de forma similar a la normalización temporal: para cada paso temporal se calcula el valor medio (centroide), la desviación estándar y la componente residual. No obstante, en este caso la vectorización se realiza transversalmente a la dimensión temporal: cada paso temporal se considera una sección espacial.
Para identificar las relaciones entre las series, especialmente aquellas que persisten tras eliminar las principales estructuras temporales (tendencia, estacionalidad y dinámica de corto plazo), se aplica un análisis de correlación. Cada serie temporal se representa en forma de matriz Tin * d, que posteriormente se vectoriza. Esto permite construir una matriz de similitud Isi ∈ RN*N, que refleja la correlación condicional entre pares de series. De este modo, el modelo es capaz de identificar grupos de series que reaccionan de forma similar ante factores externos, a pesar de la inestabilidad temporal de dichas reacciones.
Una vez finalizada la descomposición de las componentes temporales y espaciales, el modelo pasa a la fase de agregación e interpretación, en la que las componentes extraídas se combinan en una representación única para obtener el pronóstico final. Esta etapa se lleva a cabo mediante una capa de regresión polinómica, un elemento importante de la arquitectura que permite tener en cuenta las interacciones lineales y no lineales entre las componentes.
En su implementación, los autores del framework SSCNN han ampliado considerablemente la funcionalidad del módulo: a las relaciones aditivas (de suma) se han añadido dependencias multiplicativas, lo que permite modelar formas más complejas de interrelación entre las componentes, incluidos los efectos de segundo orden.
![]()
![]()
El vector Si representa una concatenación de todas las componentes obtenidas anteriormente y de los residuos correspondientes.
Esta estructura permite que la capa no se limite a sumar las características obtenidas, sino que identifique sus interdependencias y efectos combinados, lo cual resulta especialmente importante en el contexto de series temporales de alta dimensionalidad e interrelacionadas.
El resultado del procesamiento Hi es una representación generalizada en la que ya se han tenido en cuenta todos los patrones clave. Esta representación se transmite al siguiente nivel del modelo, comenzando con una nueva iteración del bloque de largo plazo Hlt i , con lo que se cierra el ciclo arquitectónico y se garantiza una dinámica predictiva integral. Este enfoque aumenta la capacidad expresiva del modelo y su capacidad para ajustarse con precisión a los datos reales del mercado, en los que la simple suma de los componentes suele resultar insuficiente.
A continuación se muestra la visualización de los autores del framework SSCNN.

Implementación con MQL5
Tras profundizar en los fundamentos teóricos del framework SSCNN, pasamos a la parte práctica del artículo. Aquí analizaremos en detalle una de las implementaciones de los métodos propuestos, aprovechando las posibilidades que ofrece el entorno MQL5. Esto nos permitirá consolidar las ideas principales y demostrar su aplicación eficaz en condiciones reales del mercado financiero.
Al estudiar la parte teórica, supongo que ya habrán observado un notable parecido conceptual entre el framework SSCNN presentado y el enfoque SCNN que analizamos anteriormente. Esta similitud se refleja incluso en los nombres de los frameworks, lo cual no es casualidad. Ambos métodos se basan en la idea de descomponer una serie temporal en componentes individuales, lo que permite analizar los datos de forma más detallada y estructurada.
Sin embargo, la diferencia fundamental radica en el propio enfoque adoptado para identificar estas componentes y extrapolarlas posteriormente. Mientras que SCNN utiliza métodos más tradicionales de descomposición y predicción, SSCNN incorpora mecanismos adaptativos innovadores, como la descomposición estructurada selectiva y los mapas de atención especializados, lo que aumenta considerablemente la precisión y la eficiencia del pronóstico.
No obstante, la extracción de la componente de largo plazo mediante una matriz rellenada con valores fijos idénticos equivale, en esencia, a la normalización clásica: el promediado a lo largo de todo el intervalo temporal. Esto simplifica la tarea y permite utilizar soluciones ya probadas y listas para usar, sin necesidad de inventar nada nuevo.
Sin embargo, la situación cambia cuando se trata de extraer componentes más complejas: la componente estacional y la componente de corto plazo. Esto requerirá mucho más esfuerzo y un ajuste fino. Para extraerlas correctamente, se necesitan mecanismos adaptativos capaces de captar la ciclicidad, la variabilidad y las características locales de la serie temporal. Esto incluye la construcción de mapas de atención especializados, la parametrización de las interrelaciones entre periodos y la consideración de las correlaciones locales.
Pasemos al elemento clave de la implementación: la escritura y el análisis del kernel de pasada directa en el lenguaje OpenCL. Es precisamente aquí, en el nivel más bajo de la arquitectura de cómputo, donde comienza el procesamiento directo de los datos, que proporciona las operaciones fundamentales de normalización y atención, sin las cuales no sería posible lograr una alta precisión y estabilidad del modelo.
Esta transición es lógica y necesaria. Ya sabemos lo importante que es identificar de forma precisa y sistemática las componentes de largo plazo, estacionales y de corto plazo de una serie temporal. Sin embargo, para implementar estos conceptos de forma eficiente y escalable, es necesario contar con una herramienta potente para el procesamiento paralelo de grandes arrays de datos. OpenCL es precisamente esa herramienta, que ofrece un acceso flexible y de alto rendimiento a los recursos de las GPU y CPU modernas.
Empezamos por crear el kernel AttentNorm, el módulo de trabajo principal encargado de aplicar los mecanismos de atención y normalización de los datos de entrada. Su función consiste en tomar los datos temporales brutos, ponderarlos mediante una matriz de atención calculada previamente, calcular las medias locales y las desviaciones estándar y, a continuación, llevar a cabo la estandarización para convertir los datos a una forma normalizada.
En los parámetros del kernel se pasan punteros a los datos que se van a analizar, los pesos de atención, los arrays para las medias y las desviaciones estándar, así como un búfer para guardar los resultados normalizados. Además, se transmiten las constantes: el número total de elementos de la secuencia individual y el tamaño del segmento que se va a procesar.
__kernel void AttentNorm(__global const float* inputs, __global const float* attention, __global float* means, __global float* stdevs, __global float* outputs, const int total_inputs, const int segment_size ) { const size_t s = get_global_id(0); const size_t i = get_local_id(1); const size_t v = get_global_id(2); const size_t total_segments = get_global_size(0); const size_t total_local = get_local_size(1); const size_t variables = get_global_size(2);
A continuación, dentro del kernel, obtenemos los índices de los flujos de ejecución actuales en tres dimensiones. La variable s indica el índice global del segmento; i, el índice local dentro del bloque; y v, el índice global de la variable o del canal. Estos valores permiten que cada flujo sepa de qué sección concreta de datos es responsable. Asimismo, se determina el número total de segmentos, el tamaño del bloque local y el número de variables, con el fin de garantizar una distribución adecuada del trabajo.
Para almacenar temporalmente los resultados intermedios, se declara un array local Temp en la memoria rápida del dispositivo. A continuación, se calcula el desplazamiento: el índice en el array de datos de origen que corresponde al flujo actual, a la variable y al segmento.
__local float Temp[LOCAL_ARRAY_SIZE]; const int shift = v * total_inputs + s * segment_size + i;
Al inicio de los cálculos se inicializan las variables para almacenar la media, la desviación estándar y el valor temporal, que se irán acumulando progresivamente. Luego se inicia un ciclo en el que cada flujo local procesa, por turnos, los elementos del segmento con un paso determinado, lo que garantiza el paralelismo y una distribución uniforme de los cálculos. Al mismo tiempo, se comprueba que no se salga de los límites para evitar errores de acceso a la memoria.
float mean = 0, stdev = 0; float val = 0; for(uint l = 0; l < segment_size; l += total_local) { if((l + i) >= segment_size || (s * segment_size + l + i) >= total_inputs) break; float val_l = IsNaNOrInf(inputs[shift + l], 0); if(l == 0) val = val_l; float att = IsNaNOrInf(attention[v * segment_size + l + i], 0); mean += val_l * att; stdev += val_l * val_l * att; }
En cada iteración del bucle, se extrae un valor del array de origen teniendo en cuenta el desplazamiento; al mismo tiempo, se aplica una función que protege contra datos no válidos, sustituyendo los valores no numéricos o infinitos por cero. En la primera iteración, el valor actual se guarda en una variable independiente para su uso posterior. Del mismo modo, se extrae del array de pesos de atención el peso correspondiente, cuya validez también se comprueba.
A continuación, se acumula la suma de los valores ponderados para calcular la media, así como la suma de los cuadrados ponderados de los valores para el posterior cálculo de la varianza. Una vez finalizado el bucle, mediante la función de suma local se realiza una reducción sobre todos los flujos locales dentro del bloque: todas las sumas parciales se combinan en una única suma, lo que garantiza un cálculo preciso de las estadísticas. Para sincronizar correctamente los flujos, se utiliza una barrera que garantiza que todos los flujos hayan finalizado sus cálculos antes de continuar.
mean = LocalSum(mean, 1, Temp); BarrierLoc; stdev = LocalSum(stdev, 1, Temp); //--- stdev -= mean * mean; stdev = IsNaNOrInf(sqrt(stdev), 1); if(stdev <= 0) stdev = 1;
El siguiente paso consiste en calcular la varianza restando el cuadrado de la media de la suma de los cuadrados; a continuación, se extrae la raíz cuadrada para obtener la desviación estándar. Es importante señalar que, si el valor de la desviación estándar resulta ser cero, negativo o no válido, se sustituye por uno para evitar errores en la normalización posterior.
A continuación, uno de los flujos locales guarda la media y la desviación estándar calculadas en los arrays de salida correspondientes, lo que garantiza que se conserven los resultados para cada segmento y cada variable.
if(i == 0) { int shift_ms = v * total_segments + s; means[shift_ms] = mean; stdevs[shift_ms] = stdev; }
A continuación, se inicia otro ciclo en el que se realiza la normalización de los datos: a cada valor de entrada se le resta la media y se divide por la desviación estándar, con una comprobación obligatoria de la validez del resultado. Los valores normalizados se guardan en el búfer de salida, listos para su posterior procesamiento.
for(uint l = 0; l < segment_size; l += total_local) { if((l + i) >= segment_size || (s * segment_size + l + i) >= total_inputs) break; if(l > 0) val = inputs[shift + l]; outputs[shift + l] = IsNaNOrInf((val - mean) / stdev, 0); } }
Este kernel distribuye eficazmente el trabajo entre los flujos, garantiza un cálculo preciso de las estadísticas teniendo en cuenta los pesos de atención y normaliza correctamente los datos, lo cual es especialmente importante al trabajar con series temporales en tareas de predicción y análisis. Gracias al uso de la memoria local y a la sincronización, se consigue un alto rendimiento y estabilidad en los cálculos.
Además, es importante señalar uno de los supuestos clave en los que se basa el algoritmo de funcionamiento de este kernel. Concretamente, se supone que el tamaño del grupo de trabajo local es igual al del segmento analizado. No se trata simplemente de una igualdad formal, sino de una condición esencial que permite simplificar la lógica computacional y mejorar la eficiencia de la ejecución del código en la GPU.
Cuando se cumple esta condición, los bucles destinados a recorrer iterativamente el segmento con un paso igual al tamaño del grupo de trabajo local se reducen, en la práctica, a una sola iteración. Esto significa que cada flujo procesa exactamente un elemento de su segmento, sin realizar pasadas repetidas. Este enfoque reduce drásticamente la complejidad de los bucles internos, simplifica la lógica y, lo que es especialmente importante, minimiza el número de accesos a la memoria global.
En lugar de leer repetidamente los datos del búfer global, cada valor del segmento se almacena en una variable local en cuanto es posible y, a partir de ahí, se utiliza desde el registro. Esto supone un nivel de rendimiento totalmente distinto: el acceso al registro o a la memoria local es un orden de magnitud más rápido que el acceso a la memoria global, sobre todo cuando hay que procesar cientos o miles de segmentos paralelos en tiempo real.
De este modo, una relación adecuada entre los tamaños del grupo de trabajo y del segmento deja de ser un mero elemento de configuración para convertirse en un factor activo de optimización. Permite aprovechar al máximo el potencial de la aceleración por hardware, reduciendo la sobrecarga y aumentando la eficiencia de todo el sistema de procesamiento de series temporales.
No obstante, a pesar de toda la elegancia y eficiencia del enfoque descrito, en la práctica no se puede confiar exclusivamente en escenarios ideales. Lamentablemente, las capacidades técnicas del hardware no siempre se ajustan a nuestros deseos. Las plataformas GPU varían en cuanto a arquitectura, cantidad de memoria disponible, limitaciones en el tamaño del grupo de trabajo local e incluso particularidades de los controladores. Todo esto hace que la igualdad exacta entre el tamaño del grupo de trabajo y la longitud del segmento sea más bien deseable que garantizada.
Precisamente por eso, en el código mantenemos el bucle, como elemento de adaptabilidad y versatilidad. Aunque en el caso óptimo solo se ejecute una vez, sigue siendo necesario para aquellas situaciones en las que el equipo no permite configurar un grupo de trabajo local del tamaño requerido. Es una especie de medida de seguridad: si el segmento resulta ser más largo que el grupo de trabajo local disponible, el ciclo lo recorrerá ordenadamente por partes, garantizando la corrección de los cálculos en cualquier circunstancia.
Este equilibrio entre eficiencia y flexibilidad permite mantener el rendimiento siempre que sea posible y, al mismo tiempo, garantizar un funcionamiento correcto incluso en dispositivos menos potentes. En el mundo de las tareas computacionales, al igual que en la vida, siempre conviene tener a mano un plan «B», sobre todo cuando se trabaja con hardware que no tiende a hacer concesiones.
Una vez finalizada la descripción de la pasada directa, en la que cada elemento se normaliza en función de la media y la desviación estándar de su segmento, pasamos a una parte más sutil y, al mismo tiempo, de importancia crítica: el algoritmo de la pasada inversa. En este caso, no basta con una simple propagación de errores. Hay que tener en cuenta que cada elemento del gradiente de salida influye en todos los elementos del segmento de entrada, ya que tanto la media como la varianza se calculan simultáneamente sobre todo el conjunto de datos. Esto impone ciertas particularidades a la implementación.
Para que quede más claro, veamos por un momento la expresión que describe la derivada del valor normalizado con respecto a la entrada.

Consta de dos términos: uno depende directamente de la derivada con respecto al valor de entrada, y el otro, de la derivada con respecto a la desviación estándar, que a su vez depende de todos los valores del segmento. Esto significa que no es posible calcular el gradiente de un elemento sin tener en cuenta la contribución de todos los demás.
Formalmente, esto se expresa en que cada gradiente de salida debe proyectarse de nuevo sobre todo el segmento, y no solo sobre su «celda» de entrada. Por lo tanto, para calcular correctamente el gradiente con respecto a un elemento de entrada concreto, es necesario recorrer todos los elementos de salida del segmento y sumar sus contribuciones, teniendo en cuenta cómo variarían la media y la desviación estándar si se modificara dicha entrada.
Un caso que reviste especial importancia es aquel en el que el índice del elemento de salida actual coincide con el índice del elemento de entrada para el que acumulamos el gradiente. En ese caso, el efecto se propaga no solo a través de la modificación de las estadísticas, sino también de forma directa, a través de la derivada de la normalización del propio valor. Si, por el contrario, los índices difieren, solo queda una influencia indirecta, que viene determinada por completo por la variación de las medias y de la varianza.
En la implementación, esto significa que, para cada elemento de entrada, debemos recorrer todos los gradientes de salida del segmento, sumando la influencia de cada uno. Este enfoque complica la estructura del kernel, pero permite lograr una correspondencia exacta con la formulación matemática. Esto es especialmente importante si queremos que el entrenamiento del modelo de red neuronal se lleve a cabo de forma correcta y eficiente.
Al pasar a analizar el algoritmo del kernel AttentNormGrad, nos adentramos en el núcleo mismo del algoritmo de retropropagación del error. Es precisamente aquí donde se calculan los gradientes con respecto a los datos de entrada y los coeficientes de atención, teniendo en cuenta la dependencia de la salida normalizada respecto al conjunto completo de valores del segmento.
__kernel void AttentNormGrad(__global const float* inputs, __global float* inputs_gr, __global const float* attention, __global float* attention_gr, __global const float* means, __global const float* stdevs, __global const float* means_gr, __global const float* outputs_gr, const int total_inputs, const int segment_size ) { const size_t i = get_global_id(0); // principal const size_t loc = get_local_id(1); // local to sum const size_t v = get_global_id(2); // variable const size_t total_main = get_global_size(0); // total const size_t total_loc = get_local_size(1); // dimensión local const size_t variables = get_global_size(2); // total de variables
Al principio del kernel se definen los índices:
- i — identificador global del elemento actual,
- loc — índice local dentro de un grupo de flujos,
- v — número de la variable con la que estamos trabajando.
__local float Temp[LOCAL_ARRAY_SIZE]; //--- Gradiente de las entradas { const int s = i / segment_size; const int shift_in = v * total_inputs + i; const int shift_ms = v * segment_size + s; float grad = 0; if(loc == 0 && i < total_inputs) { Temp[0] = IsNaNOrInf(inputs[shift_in], 0); Temp[1] = IsNaNOrInf(means[shift_ms], 0); Temp[2] = IsNaNOrInf(stdevs[shift_ms], 1); Temp[3] = IsNaNOrInf(means_gr[shift_ms], 0); Temp[4] = IsNaNOrInf(attention[(v - s) * segment_size + i], 0); } BarrierLoc;
A continuación comienza la primera parte del kernel: el cálculo de los gradientes para los datos de entrada inputs. Aquí determinamos a qué segmento pertenece el elemento i, calculamos los desplazamientos necesarios en la memoria y, si nos encontramos en el primer flujo dentro del grupo de trabajo local (loc == 0), cargamos los valores necesarios en el búfer local: el propio valor de entrada, la media, la desviación estándar, el gradiente de la media y el coeficiente de atención correspondiente. A continuación, se aplica una barrera de sincronización para que todos los flujos esperen a que se carguen los datos y puedan trabajar con ellos de forma segura.
Ahora empieza la parte más importante del trabajo. Recorremos todo el segmento y analizamos la contribución de cada salida normalizada al gradiente del valor de entrada que nos interesa. En este sentido, se presta especial atención a si las posiciones coinciden, es decir, a si se trata del mismo elemento con respecto al cual calculamos el gradiente (same). Si es así, su contribución se calcula directamente; si no, solo de forma indirecta, a través de la variación de las estadísticas del segmento. Para ello, en el cálculo se tienen en cuenta tanto la desviación del valor respecto a la media como su influencia en la desviación estándar. Todo esto se acumula cuidadosamente en la variable grad. Asimismo, tenemos en cuenta el gradiente con respecto a la media, acumulado por otros flujos de información.
if(i < total_inputs) { float x = Temp[0]; float mean = Temp[1]; float stdev = Temp[2]; float mean_gr = Temp[3]; float att = Temp[4]; for(int l = 0; l < segment_size; l += total_loc) { if((l + loc) >= segment_size || (i * segment_size + loc + l) >= total_inputs) break; float out_gr = IsNaNOrInf(outputs_gr[v * total_inputs + s * segment_size + loc + l], 0); bool same = (i - s * segment_size) == (loc + l); float xl = x; if(!same) xl = IsNaNOrInf(inputs[v * total_inputs + s * segment_size + loc + l], 0); float dy = ((int)same - att) * (1 / stdev - (xl - mean) * att * x / pow(stdev, 3.0f)); float dmean = (same ? IsNaNOrInf(mean_gr * att, 0) : 0); grad += IsNaNOrInf(dy * out_gr + dmean, 0); } } grad = LocalSum(grad, 1, Temp); if(loc == 0 && i < total_inputs) inputs_gr[shift_in] = grad; BarrierLoc; }
Los resultados obtenidos se suman localmente y se guardan en el búfer inputs_gr.
Una vez que hemos terminado con los gradientes con respecto a los datos de entrada, pasamos a la segunda parte: el cálculo de los gradientes con respecto a los coeficientes de atención. Aquí todo es simétrico: la lógica es similar, pero el énfasis cambia. Ahora, para cada i (elemento del segmento), recorremos todos los datos de entrada y observamos cómo varía la salida al modificar el coeficiente de atención. La estructura sigue siendo la misma: cálculo del gradiente local, comprobación de las condiciones de coincidencia y consideración de la contribución mediante la variación de la desviación estándar y la media. Y de nuevo: una suma local cuidadosa y el almacenamiento del resultado en attention_gr.
//--- Gradiente de atención { float grad = 0; int shift_att = v * segment_size + i; if(i < segment_size) { float att = IsNaNOrInf(attention[shift_att], 0); for(int l = 0; l < total_inputs; l += total_loc) { if((l + loc) >= total_inputs) break; int shift_out = (l + loc) + v * total_inputs; int s = (l + loc) / segment_size; int shift_in = v * total_inputs + s * segment_size + i; float x = IsNaNOrInf(inputs[shift_in], 0); float out_gr = IsNaNOrInf(outputs_gr[shift_out], 0); float mean = means[v * segment_size + s]; float stdev = stdevs[v * segment_size + s]; float mean_gr = means_gr[v * segment_size + s]; bool same = (i - s * segment_size) == (loc + l); float xl = x; if(!same) xl = IsNaNOrInf(inputs[shift_out], 0); float dy = -x / stdev - (xl - mean) * x * x * (1 - 2 * att) / (2 * pow(stdev, 3.0f)); float dmean = IsNaNOrInf(mean_gr * x, 0); grad += IsNaNOrInf(dy * out_gr + dmean, 0); } } grad = LocalSum(grad, 1, Temp); if(loc == 0 && i < segment_size) attention_gr[shift_att] = grad; } }
Nuestro kernel implementa la expresión del gradiente de acuerdo con la fórmula derivada anteriormente. El código en su conjunto no es simplemente un conjunto de comandos, sino un sistema minuciosamente organizado. Cada flujo de ejecución evalúa cuidadosamente cómo la variación de cada entrada individual o de cada coeficiente de atención afecta a las salidas de todo el grupo. Aquí intervienen la optimización mediante búferes locales, una sincronización precisa y el tratamiento cuidadoso de las condiciones de contorno. Así es precisamente como se presenta una verdadera solución de ingeniería: rigurosa, bien calibrada, pero a la vez adaptable a las capacidades del hardware y a las particularidades de la diferenciación numérica.
Hemos analizado en detalle la implementación de un componente clave del framework SSCNN: el módulo de normalización con atención en el lado del programa en OpenCL. Ante nuestros ojos, la teoría se transformó en un algoritmo funcional, capaz de tener en cuenta el peso de cada elemento y distribuir la influencia por toda la estructura de datos con una precisión digna de la alta ingeniería. Hemos podido comprobar lo importante que es ajustar con precisión la interacción entre la memoria local y la global, lo fundamental que es tener en cuenta las particularidades de la arquitectura del hardware y por qué incluso una operación de normalización, aparentemente sencilla, se convierte en un auténtico problema computacional en un entorno paralelo.
Sin embargo, nuestro camino aún no ha terminado. La normalización es solo una de las facetas de un mecanismo multicomponente en el que se basa todo el framework SSCNN. En el próximo artículo seguiremos trabajando. Lo más interesante no ha hecho más que empezar.
Conclusión
En este artículo hemos analizado los aspectos teóricos del framework SSCNN, en el que se presta especial atención a la idea de la descomposición estructural de una serie temporal y a la extracción de componentes mediante normalización basada en un mecanismo de atención. Los autores del framework han propuesto una arquitectura bien fundamentada, capaz de trabajar con series temporales de alta dimensionalidad con costes computacionales mínimos.
En la parte práctica del artículo hemos analizado en detalle la implementación de uno de los componentes clave del framework: la capa T-AttnNorm. Se prestó especial atención a las características de la pasada directa y la pasada inversa, así como a la interacción con la memoria global y local. Se han identificado los matices del comportamiento del algoritmo en casos particulares y se han aportado comentarios prácticos sobre su adaptación a la arquitectura OpenCL.
Hemos sentado las bases para la implementación del stack completo SSCNN y hemos proporcionado una base para su capacidad de aprendizaje, confirmando la importancia de recopilar gradientes con precisión y calcular derivadas. En el próximo artículo continuaremos nuestro recorrido.
Referencias
- Parsimony or Capability? Decomposition Delivers Both in Long-term Time Series Forecasting
- Otros artículos de la serie
Programas utilizados en el artículo
| # | Nombre | Tipo | Descripción |
|---|---|---|---|
| 1 | Study.mq5 | Asesor experto | Asesor experto para el entrenamiento offline de modelos |
| 2 | StudyOnline.mq5 | Asesor experto | Asesor experto para el entrenamiento online de modelos |
| 3 | Test.mq5 | Asesor experto | Asesor experto para probar el modelo |
| 4 | Trajectory.mqh | Biblioteca de clases | Estructura para describir el estado del sistema y la arquitectura de los modelos |
| 5 | NeuroNet.mqh | Biblioteca de clases | Biblioteca de clases para crear una red neuronal |
| 6 | NeuroNet.cl | Biblioteca | Biblioteca de código del programa OpenCL |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/19069
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.
Robot de trading basado en redes neuronales sobre la moderna arquitectura Mamba con modelo selectivo de espacio de estados (SSM)
Gestor de riesgos para robots de trading (Parte I): archivo include para el control de riesgos en asesores expertos
Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 8): Análisis de múltiples estrategias (3) — Política de votación ponderada
Predicción en el trading y modelos de Grey
- 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