English Русский 中文 Deutsch 日本語 Português
preview
Redes neuronales: así de sencillo (Parte 17): Reducción de la dimensionalidad

Redes neuronales: así de sencillo (Parte 17): Reducción de la dimensionalidad

MetaTrader 5Sistemas comerciales | 18 agosto 2022, 16:38
417 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Contenido

Introducción

Continuamos profundizando en el estudio de modelos y algoritmos de aprendizaje no supervisado. Ya hemos analizado los algoritmos de clusterización de datos. En este artículo, queremos presentarle una solución al problema de la reducción de la dimensionalidad. En esencia, hablaremos de ciertos algoritmos de compresión de datos que se usan ampliamente en la práctica. Vamos a ver la implementación de uno de estos algoritmos y la posibilidad de usarlo en la construcción de nuestro modelo comercial.


1. Entendiendo el problema de la reducción de la dimensionalidad

Cada nuevo día, cada nueva hora y cada nuevo momento nos ofrece una enorme cantidad de información en todas las esferas de la vida humana. En este mundo gobernado por las tecnologías de la información, la gente almacena y procesa la mayor cantidad de información posible. Y para guardar grandes cantidades de información, se requieren repositorios de datos de gran capacidad. Al mismo tiempo, el procesamiento de grandes cantidades de datos requiere los recursos informáticos correspondientes. Y, naturalmente, existe el deseo de encontrar una forma de registrar la información disponible de una manera más compacta. Además, si la forma comprimida contiene el contexto completo de la información, se requerirán menos recursos para su procesamiento.

Por ejemplo, estamos trabajando con el reconocimiento de patrones en una imagen de 200 * 200 píxeles de tamaño, y cada píxel está escrito en el formato color, que ocupa 4 bytes en la memoria. La capacidad de representación de cada píxel en uno de los 16,5 millones de colores resulta claramente excesiva para nuestra tarea. Y en la mayoría de los casos, el rendimiento de nuestro modelo no se verá afectado si reducimos la gradación, digamos, a 16 o 32 colores. En este caso, para registrar el número de color de cada píxel, será suficiente con 1 byte. Por supuesto, necesitaremos un coste único para escribir nuestra matriz de colores; 64 bytes para 16 colores y 128 bytes para 32 colores. Estará de acuerdo con que este no es un gran precio a pagar por comprimir 4 veces todas nuestras imágenes. Si lo piensa, un problema similar podrá resolverse usando el método de clusterización de datos que ya conocemos, aunque esta no sea la forma más eficiente.

Otra área de uso de las técnicas de reducción de la dimensionalidad sería la visualización de datos. Por ejemplo, tenemos los datos que describen los estados de un determinado sistema representado por 10 parámetros. Y necesitamos encontrar una forma de visualizar estos datos. Para la percepción humana, las imágenes en 2D y en 3D son las preferidas. Por supuesto, podemos crear varias diapositivas con diferentes variaciones de 2-3 parámetros, pero esto no dará una imagen completa del estado del sistema. Y en la mayoría de los casos, distintos estados en diferentes diapositivas se fusionarán en un solo punto, y no siempre serán los mismos estados.

Por lo tanto, querríamos encontrar un algoritmo que nos ayude a pasar todos los estados de nuestro sistema de 10 parámetros a un espacio de 2 o 3 dimensiones. Al mismo tiempo, también querríamos dividir nuestros estados del sistema tanto como resulte posible manteniendo su posición relativa. Y por supuesto, con la mínima pérdida de información.

Tal vez pensará usted: "Claro que todo esto es interesante, pero ¿cuál es su uso práctico en el trading?" Echemos un vistazo a nuestro terminal para ver cuántos indicadores nos ofrece. Sí, muchos de ellos tienen hasta cierto punto correlación de datos, pero cada uno de ellos nos ofrece al menos un valor de la descripción de nuestra situación de mercado. ¿Y si multiplicamos esto por el número de instrumentos comerciales? Las distintas variaciones de los parámetros de los indicadores, así como los marcos temporales analizados, podrían aumentar hasta el infinito el número de parámetros para describir el estado actual del mercado.

Sin duda, no vamos a estudiar a la vez todos los instrumentos e indicadores posibles en un modelo. No obstante, en busca de la combinación más adecuada de estos, podemos usar un número bastante elevado de instrumentos, cosa que complicará nuestro modelo y su tiempo de entrenamiento. Por lo tanto, reducir la dimensionalidad de los datos iniciales manteniendo el máximo de información es una forma directa tanto de reducir el coste de entrenamiento del modelo como de reducir el tiempo en la toma de decisiones. Esto significará que la reacción al comportamiento del mercado será muy rápida, y las transacciones se realizarán al precio más favorable.

También debemos decir que los algoritmos de reducción de la dimensionalidad siempre se usan solo para el preprocesamiento de datos, ya que retornan solo una forma comprimida de los datos originales, que luego son almacenados o transmitidos para su posterior procesamiento. Puede ser la visualización de datos o el procesamiento por parte de algún otro modelo.

Así, para construir nuestro sistema comercial, podemos tomar la máxima cantidad de información necesaria para describir el estado actual del mercado, y luego comprimirla usando uno de los algoritmos de reducción de dimensionalidad. Al mismo tiempo, esperamos que parte del ruido y los datos correlacionados pueda eliminarse durante el proceso de compresión. Los datos comprimidos los suministraremos como entrada a nuestro modelo de toma de decisiones comerciales.

Esperamos que la idea esté clara. Para su implementación, proponemos tomar como algoritmo de reducción de la dimensionalidad uno de los métodos más comunes del análisis de componentes principales. Este algoritmo ha demostrado su eficacia en la resolución de varios problemas y se puede replicar en nuevos datos. Esto nos permitirá comprimir los datos entrantes y transferirlos al modelo de toma de decisiones para generar decisiones comerciales en tiempo real.

2. Método de análisis de componentes principales (APC)

El análisis de componentes principales fue inventado por el matemático inglés Karl Pearson en 1901. Desde entonces, se ha usado con éxito en muchos campos de la ciencia.

Para comprender la esencia del método, proponemos realizar una tarea simplificada para reducir la dimensionalidad de un array bidimensional de datos a un vector. Desde un punto de vista geométrico, esto se puede representar como una proyección de los puntos de un cierto plano sobre una línea recta.

En la siguiente figura, los datos iniciales están representados por puntos azules y se realizan dos proyecciones sobre las líneas naranja y gris con los puntos del color correspondiente. Como podemos ver, la distancia promedio de los puntos iniciales hasta sus proyecciones naranjas será menor que las distancias similares hasta las proyecciones grises. En este caso, entre las proyecciones grises, podemos notar la superposición de las proyecciones de los puntos entre sí. Por lo tanto, la proyección naranja resulta preferible para nosotros, ya que separa todos los puntos individuales y sufre una menor pérdida de datos al reducir la dimensionalidad (la distancia de los puntos hasta sus proyecciones).

Esa línea se llama componente principal. De ahí el nombre del método de análisis de componentes principales.

Desde un punto de vista matemático, cada componente principal es un vector numérico con un tamaño igual a la dimensión de los datos originales. El producto del vector de datos iniciales que describe un estado del sistema por el vector correspondiente del componente principal da precisamente el punto de proyección del estado analizado en la línea recta.

Según la dimensionalidad de los datos de origen y los requisitos para la compresión de los datos, podrá haber varios de estos componentes principales, pero no más que la dimensionalidad de los datos de origen. Al visualizar una proyección volumétrica, habrá 3 de ellos. Al comprimir los datos, parten de un error permitido, por lo general teniendo una pérdida de hasta el 1% de los datos.

Método de componentes principales

Probablemente deberíamos prestar atención a que esto resulta visualmente similar a la regresión lineal. Pero estos son métodos completamente distintos y dan resultados diferentes.

Al construir una regresión lineal, se muestra una dependencia lineal del valor de una variable con respecto a otra, y se minimizan las distancias hasta la recta perpendicular a los ejes de coordenadas. Esta línea puede pasar en cualquier parte del plano.

En el análisis de componentes principales, los valores a lo largo de todos los ejes son absolutamente independientes y equivalentes. Las distancias que son perpendiculares a la propia línea, y no a los ejes de coordenadas, son minimizadas. Y la línea del componente principal pasa necesariamente por el origen de las coordenadas. Por lo tanto, todos los datos iniciales deberán normalizarse antes de aplicar el método, o al menos deberán centrarse respecto al origen de las coordenadas. En otras palabras, necesitaremos centrar los datos relativos a "0" en cada dimensión. 

Otra característica importante del método de análisis de componentes principales es que, como resultado de su aplicación, obtenemos una matriz de vectores ortogonales de componentes principales. Es decir, no existe correlación entre todos los vectores de los componentes principales. Este hecho influye positivamente en todo el proceso de aprendizaje del modelo posterior de toma de decisiones, que recibe como entrada los datos comprimidos.

Desde un punto de vista matemático, podemos representar el método de análisis de componentes principales como la descomposición espectral de la matriz de covarianza de los datos iniciales. Y la matriz de covarianza es fácil de encontrar según la fórmula.

Fórmula de la matriz de covarianza

dónde

  • C es la matriz de covarianza,
  • X es la matriz de datos iniciales,
  • n es el número de elementos en los datos originales.

Como resultado de esta operación, obtenemos una matriz de covarianza cuadrada, cuyo tamaño es igual al número de signos que describen el estado del sistema. Las varianzas de las características se ubicarán en la diagonal principal de esta matriz, mientras que los elementos restantes de la matriz representarán el grado de covarianza de los pares de características correspondientes.

En la siguiente etapa, tendremos que implementar la descomposición en valores singulares de la matriz de covarianza resultante. En sí misma, la descomposición en valores singulares de una matriz es un proceso matemático bastante complejo. Pero la introducción de matrices y operaciones con matrices en el lenguaje MQL5 ha simplificado enormemente este proceso para nosotros, ya que esta operación ha sido implementada finalmente para matrices. Por lo tanto, analizaremos de inmediato los resultados de la descomposición en valores singulares.

Descomposición en valores singulares de una matriz

Como resultado de la descomposición en valores singulares de la matriz, obtenemos 3 matrices, cuyo producto será igual a la matriz original. La segunda matriz ∑ es una matriz diagonal con el mismo tamaño que la matriz original. A lo largo de la diagonal principal de esta matriz se encuentran números singulares que representan la varianza de los valores a en los ejes de vectores singulares. Los números singulares no son negativos y están colocados en orden descendente. Todos los demás elementos de la matriz son iguales a "0". Por lo tanto, con frecuencia se representa como un vector.

U y V son matrices cuadradas unitarias que contienen los vectores singulares izquierdo y derecho, respectivamente. El tamaño de la matriz U es igual al número de filas de la matriz original, mientras que el tamaño de la matriz V es igual al número de columnas de la matriz original.

En nuestro caso particular, al realizar la descomposición en valores singulares de la matriz de covarianza cuadrada, las matrices U y Vson del mismo tamaño.

Para reducir la dimensionalidad de los datos, usaremos la matriz U. Como los valores singulares están en orden descendente, bastará con tomar el número requerido de primeras columnas de la matriz U. Denotaremos la nueva matriz como la matriz UR . Para reducir la dimensionalidad, bastará con multiplicar la matriz de datos inicial por la matriz UR que hemos creado.

Reducción de la dimensionalidad

Y aquí surge la pregunta: ¿a qué valor es óptimo reducir la dimensionalidad? Si nuestra tarea consiste en visualizar datos, entonces no nos enfrentaremos a esta pregunta, puesto que la elección de la dimensionalidad de 1 a 3 dependerá de la proyección que estemos creando. Sin embargo, nosotros estamos planeando comprimir los datos con una mínima pérdida de información para su posterior transferencia a otro modelo de decisión. Por lo tanto, el criterio principal para nosotros será la cantidad de información perdida.

La mejor opción para determinar la cantidad de información almacenada será calcular la proporción de valores singulares correspondientes a los vectores singulares usados.

Porcentaje de información transmitida

dónde

  • k es el número de vectores usados,
  • N es el número total de valores singulares.

En la práctica, el número de columnas k se elige de forma que el valor de la relación anterior sea al menos de 0,99, lo cual corresponde a una preservación del 99% de la información.

Ahora que nos hemos familiarizado con los aspectos teóricos generales, podemos comenzar a implementar el método.


3. Implementación del APC usando MQL5

Para implementar el algoritmo de análisis de componentes principales, crearemos una nueva clase CPCA, heredera de la clase básica CObject, y guardaremos todo el código de la nueva clase en el archivo "pca.mqh".

Debemos decir que en la implementación de esta clase usaremos operaciones matriciales. Por lo tanto, guardaremos el resultado del entrenamiento de nuestro modelo, o mejor dicho la matriz UR, en la matriz m_Ureduce.

Además, declararemos 3 variables locales adicionales. Hablamos, en primer lugar, de la bandera que indica el estado del entrenamiento del modelo b_Studied. Las otras dos variables son 2 vectores v_Means yv_STDs, en los que guardaremos los valores de las medias aritméticas y las desviaciones estándar para la posterior normalización de datos.

class CPCA : public CObject
  {
private:
   bool              b_Studied;
   matrix            m_Ureduce;
   vector            v_Means;
   vector            v_STDы;

En el constructor de la clase, indicaremos el valor false en la bandera de estado del entrenamiento del modelo b_Studied e inicializaremos la matriz m_Ureduce con un tamaño cero. Asimismo, dejaremos el destructor de clases vacío, ya que no estamos creando ningún objeto anidado dentro de la clase.

CPCA::CPCA()   :  b_Studied(false)
  {
   m_Ureduce.Init(0, 0);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPCA::~CPCA()
  {
  }

A continuación, recrearemos el método de entrenamiento del modelo Study. En los parámetros, el método obtiene una matriz con los datos iniciales. Y luego retorna el resultado lógico de la operación.

Como hemos mencionado anteriormente, para realizar un análisis de componentes principales, deberemos utilizar los datos normalizados. Por lo tanto, antes de comenzar a implementar el algoritmo principal del método, normalizaremos los datos iniciales usando la fórmula a continuación. 

Normalización de datos

El uso de operaciones matriciales también simplificará esta tarea. Ahora no necesitaremos crear sistemas de ciclos. Para encontrar los valores medios aritméticos para todas las características, bastará con usar el método de operaciones de matriz Mean con una indicación de la medida para calcular los valores. Como resultado de la operación, obtendremos inmediatamente un vector con los valores medios aritméticos de todas las características.

En el denominador de nuestra fórmula de normalización de datos, podemos ver la raíz cuadrada de la varianza, que se corresponde con la desviación estándar. Aquí es donde las operaciones matriciales resultan útiles. El método STD retorna un vector de desviaciones estándar para la dimensión especificada. Solo necesitamos añadir una pequeña constante para eliminar el error de división por cero.

Guardaremos los vectores resultantes en las variables correspondientes v_Means y v_STDs. Después de todo, necesitaremos realizar una normalización similar de los datos iniciales tanto en la etapa de entrenamiento del modelo como en la etapa de operación.

A continuación, normalizaremos directamente los datos. Para ello, prepararemos una matriz X igual al tamaño de los datos de origen, y organizaremos un ciclo con un número de iteraciones igual al número de filas en la matriz con los datos de origen.

En el cuerpo del ciclo, normalizaremoslos datos iniciales y almacenaremos el resultado de las operaciones en la matriz X previamente creada. El uso de operaciones vectoriales también nos ayudará a eliminar la necesidad de crear un ciclo anidado.

bool CPCA::Study(matrix &data)
  {
   matrix X;
   ulong total = data.Rows();
   if(!X.Init(total,data.Cols())
      return false;
   v_Means = data.Mean(0);
   v_STDs = data.STD(0) + 1e-8;
   for(ulong i = 0; i < total; i++)
     {
      vector temp = data.Row(i) - v_Means;
      temp /= v_STDs;
      X = X.Row(temp, i);
     }


Después de normalizar los datos iniciales, procederemos directamente a la implementación del algoritmo de análisis de componentes principales. Como hemos mencionado antes, en la primera etapa tendremos que calcular la matriz de covarianza. Gracias al uso de operaciones matriciales, esto encajará fácilmente en una línea de código. Para no crear objetos innecesarios, sobrescribiremos el resultado de las operaciones en nuestra matriz X.

   X = X.Transpose().MatMul(X / total);

Según el algoritmo anterior, la siguiente operación debería ser la descomposición en valores singulares de la matriz de covarianza. Como resultado de esta operación, esperamos obtener 3 matrices: vectores singulares izquierdos, valores singulares y vectores singulares derechos. Como el lector sin duda recordará, los elementos de la matriz de valores singulares solo pueden tener valores distintos de cero a lo largo de la diagonal principal. Por consiguiente, para ahorrar recursos en la implementación de MQL5, en lugar de una matriz, se retornará un vector de valores singulares.

Antes de llamar a la función, declararemos 2 matrices y 1 vector para recibir los resultados, y solo entonces llamaremos al método matricial de descomposición en valores singulares SVD. En los parámetros del método, transmitiremos las matrices y el vector necesarios para registrar los resultados de la operación.

   matrix U, V;
   vector S;
   if(!X.SVD(U, V, S))
      return false;

Ahora que hemos obtenido matrices ortogonales de vectores singulares, necesitaremos determinar a qué nivel reduciremos la dimensionalidad de los datos originales. Como norma general, conservaremos al menos el 99 % de la información contenida en los datos originales.

Siguiendo la lógica anterior, primero determinaremos la suma total de todos los elementos del vector de valores singulares. Al mismo tiempo, verificaremos necesariamente que el valor resultante sea mayor que "0". Por definición, no puede ser negativo, ya que los números singulares no son negativos. Además, deberemos excluir el error de división por "0".

Después de eso, calcularemos las sumas acumuladas de los valores del vector de valores singulares y dividiremos el vector resultante por la suma total de valores singulares.

Como resultado, obtendremos un vector de valores crecientes con un máximo igual a "1".

Ahora, para determinar el número de columnas requeridas, solo nos queda encontrar la posición del primer elemento en el vector que sea mayor o igual que el valor umbral para guardar información. En el ejemplo mostrado, el valor será 0,99. Eso asegura la preservación del 99% de la información original. 

   double sum_total = S.Sum();
   if(sum_total<=0)
      return false;
   S = S.CumSum() / sum_total;
   int k = 0;
   while(S[k] < 0.99)
      k++;

Solo tenemos que redimensionar la matriz y trasladar su contenido a la matriz de nuestra clase. Después de eso, cambiaremos la bandera de entrenamiento del modelo y saldremos del método.

   if(!U.Resize(U.Rows(), k + 1))
      return false;
//---
   m_Ureduce = U;
   b_Studied = true;
   return true;
  }

Una vez hayamos creado el método de entrenamiento del modelo, es decir, una vez hayamos determinado la matriz de reducción de la dimensionalidad de los datos originales, también podremos crear el método ReduceM para reducir la dimensionalidad de los datos de entrada. Este recibe los datos iniciales en los parámetros y retorna una matriz de dimensionalidad reducida.

Obviamente, los datos de entrada deberán ser comparables con los datos usados para entrenar el modelo. Aquí estamos hablando de la cantidad y calidad de los signos que describen el estado del sistema, no del número de observaciones.

Al inicio del método, crearemos un bloque de controles en el que verificaremos la bandera de entrenamiento del modelo y la igualdad del número de columnas en la matriz de datos de entrada (número de características) con el número de filas en nuestra matriz de reducción m_Ureduce. Si alguna de las condiciones no se cumple, saldremos del método y retornaremos una matriz de tamaño cero.

matrix CPCA::ReduceM(matrix &data)
  {
   matrix result;
   if(!b_Studied || data.Cols() != m_Ureduce.Rows())
      return result.Init(0, 0);

Después de pasar con éxito el bloque de controles, necesitaremos normalizar los datos originales antes de realizar la reducción de la dimensionalidad. El algoritmo de normalización será similar al analizado anteriormente al entrenar el modelo, solo que esta vez no calcularemos la media aritmética ni la desviación estándar, sino que usaremos los vectores correspondientes guardados durante el entrenamiento. De esta forma, aseguraremos la comparabilidad de los nuevos resultados y los obtenidos durante el entrenamiento.

   ulong total = data.Rows();
   if(!X.Init(total,data.Cols()))
      return false;
   for(ulong r = 0; r < total; r++)
     {
      vector temp = data.Row(r) - v_Means;
      temp /= v_STDs;
      result = result.Row(temp, r);
     }

Antes de completar el algoritmo del método, nos queda multiplicar la matriz de valores normalizados por la matriz de reducción y retornar el resultado de la operación al programa que realiza la llamada.

   return result.MatMul(m_Ureduce);
  }

Así hemos construido los métodos para entrenar el modelo de reducción de la dimensionalidad de los datos originales. Gracias al uso de operaciones matriciales, el código ha resultado bastante conciso, y hemos evitado profundizar en sutilezas matemáticas. Sin embargo, no debemos olvidar que este es el primer código en nuestra biblioteca que usa operaciones matriciales, y que antes de él, usábamos arrays dinámicos en los objetos CBufferDouble. Por lo tanto, para organizar la compatibilidad de nuestros objetos, deberemos crear una interfaz para transferir los datos de un búfer dinámico a una matriz y viceversa.

Para organizar este proceso, crearemos dos métodos, FromBuffer yFromMatrix. El primer método tomará como parámetros un búfer de datos dinámico y el tamaño del vector que describe un estado del sistema, mientras que retornará una matriz a la que se transferirá el contenido del búfer.

En el cuerpo del método, primero organizaremos un bloque de controles en el que verificaremos en primer lugar la validez del puntero al objeto de búfer de los datos originales. Luego comprobaremos la multiplicidad del tamaño del búfer por el tamaño del vector de descripción de un estado del sistema analizado.

matrix CPCA::FromBuffer(CBufferDouble *data, ulong vector_size)
  {
   matrix result;
   if(CheckPointer(data) == POINTER_INVALID)
     {
      result.Init(0, 0);
      return result;
     }
//---
   if((data.Total() % vector_size) != 0)
     {
      result.Init(0, 0);
      return result;
     }

Después de superar todas las comprobaciones, determinaremos el número de filas en la matriz e inicializaremos la matriz resultante.

   ulong rows = data.Total() / vector_size;
   if(!result.Init(rows, vector_size))
     {
      result.Init(0, 0);
      return result;
     }

A continuación, organizaremos un sistema de ciclos anidados en el que transferiremos todo el contenido del búfer dinámico a la matriz.

   for(ulong r = 0; r < rows; r++)
     {
      ulong shift = r * vector_size;
      for(ulong c = 0; c < vector_size; c++)
         result[r, c] = data[(int)(shift + c)];
     }
//---
   return result;
  }

Una vez completado el sistema del ciclo, saldremos del método y retornaremos la matriz creada al programa que ha realizado la llamada.

El segundo método, FromMatrix, se encarga de realizar la operación inversa. En los parámetros del método, transmitiremos la matriz con los datos, y en la salida obtendremos un búfer de datos dinámico.

En el cuerpo del método, primero crearemos un nuevo objeto de matriz dinámica e inmediatamente verificaremos el resultado de la operación.

CBufferDouble *CPCA::FromMatrix(matrix &data)
  {
   CBufferDouble *result = new CBufferDouble();
   if(CheckPointer(result) == POINTER_INVALID)
      return result;

Luego reservaremos un tamaño del array dinámico suficiente para almacenar todo el contenido de la matriz.

   ulong rows = data.Rows();
   ulong cols = data.Cols();
   if(!result.Reserve((int)(rows * cols)))
     {
      delete result;
      return result;
     }

A continuación, solo tendremos que transferir el contenido de la matriz al array dinámico. Esta operación se realizará en un sistema de dos ciclos anidados.

   for(ulong r = 0; r < rows; r++)
      for(ulong c = 0; c < cols; c++)
         if(!result.Add(data[r, c]))
           {
            delete result;
            return result;
           }
//---
   return result;
  }

Una vez todas las operaciones del ciclo se hayan completado con éxito, saldremos del método y retornaremos el objeto de búfer de datos creado al programa que realiza la llamada.

Aquí, tenemos que decir que no almacenaremos el puntero al objeto creado. Por consiguiente, todo el trabajo relacionado con el monitoreo de su estado y su eliminación de la memoria una vez completado el trabajo, deberá organizarse en el lado del programa que realiza la llamada.

Vamos a crear métodos similares para trabajar con vectores. Transferiremos datos del búfer al vector con el método FromBuffer sobrecargado. Luego, organizaremos la operación inversa en el método FromVector. Los algoritmos para construir los métodos son similares a los mostrados anteriormente. Podrá encontrar el código completo de los métodos en el archivo adjunto.

Después de crear los métodos de transferencia de datos, podremos crear una sobrecarga del método de entrenamiento del modelo, que recibirá en los parámetros un búfer de datos dinámico y el tamaño del vector de descripción de un estado del sistema. El algoritmo para construir ese método resulta bastante simple. Primero transferiremos los datos del búfer dinámico a la matriz usando el método FromBuffer analizado anteriormente. Y luego llamaremos al método analizado anteriormente para entrenar el modelo, transmitiéndole la matriz resultante.

bool CPCA::Study(CBufferDouble *data, int vector_size)
  {
   matrix d = FromBuffer(data, vector_size);
   return Study(d);
  }

Vamos a crear una sobrecarga similar para el método de reducción de la dimensionalidad ReduceM. Solo que, a diferencia de la sobrecarga analizada del método de entrenamiento, en los parámetros de este método solo transmitiremos el búfer de los datos iniciales sin especificar el tamaño del vector de descripción de un estado del sistema analizado. Esto se debe a que en este momento el modelo ya debe estar entrenado y el tamaño del vector de descripción del estado del sistema deberá ser igual al número de filas en la matriz de reducción.

Otra diferencia de este método es que para evitar una transferencia de datos excesiva, primero verificaremos si el modelo está entrenado; luego comprobaremos la multiplicidad del tamaño del búfer respecto al tamaño del vector de descripción del estado del sistema. Y solo después de superar los controles, llamaremos al método de transferencia de datos.

matrix CPCA::ReduceM(CBufferDouble *data)
  {
   matrix result;
   result.Init(0, 0);
   if(!b_Studied || (data.Total() % m_Ureduce.Rows()) != 0)
      return result;
   result = FromBuffer(data, m_Ureduce.Rows());
//---
   return ReduceM(result);
  }

Para obtener una matriz de dimensiones reducidas en forma de búfer de datos dinámicos, crearemos 2 métodos Reduce sobrecargados adicionales. Uno de los parámetros recibirá el búfer de datos dinámico de los datos originales. El segundo será la matriz, cuyo código mostramos abajo. 

CBufferDouble *CPCA::Reduce(CBufferDouble *data)
  {
   matrix result = ReduceM(data);
//---
   return FromMatrix(result);
  }

CBufferDouble *CPCA::Reduce(matrix &data)
  {
   matrix result = ReduceM(data);
//---
   return FromMatrix(result);
  }

Podría parecer extraño, pero a pesar de la diferencia en los parámetros de los métodos, su contenido es exactamente el mismo. Esto se explica fácilmente usando las sobrecargas anteriores del método ReduceM.

Ya nos hemos ocupado de la funcionalidad de la clase. Ahora solo tenemos que crear los métodos para trabajar con archivos. Recordemos que cualquier modelo, una vez entrenado, debería poder restaurar rápidamente su funcionamiento para su uso posterior. Como siempre, comenzaremos con el método Save.

No obstante, antes de continuar con la construcción del algoritmo del método de guardado de datos, echaremos un vistazo a la estructura de nuestra clase y pensaremos qué debemos guardar en un archivo.

Entre las variables de clase privada, podemos ver la bandera de entrenamiento del modelo b_Studied, la matriz de reducción m_Ureduce y dos vectores con medias aritméticas v_Means y la desviación estándar v_STDs. Y para restaurar completamente el funcionamiento del modelo, necesitaremos guardar todos estos elementos.

class CPCA : public CObject
  {
private:
   bool              b_Studied;
   matrix            m_Ureduce;
   vector            v_Means;
   vector            v_STDs;
   //---
   CBufferDouble     *FromMatrix(matrix &data);
   CBufferDouble     *FromVector(vector &data);
   matrix            FromBuffer(CBufferDouble *data, ulong vector_size);
   vector            FromBuffer(CBufferDouble *data);

public:
                     CPCA();
                    ~CPCA();
   //---
   bool              Study(CBufferDouble *data, int vector_size);
   bool              Study(matrix &data);
   CBufferDouble     *Reduce(CBufferDouble *data);
   CBufferDouble     *Reduce(matrix &data);
   matrix            ReduceM(CBufferDouble *data);
   matrix            ReduceM(matrix &data);
   //---
   bool              Studied(void)  {  return b_Studied; }
   ulong             VectorSize(void)  {  return m_Ureduce.Cols();}
   ulong             Inputs(void)   {  return m_Ureduce.Rows();   }
   //---
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   //---
   virtual int       Type(void)  { return defUnsupervisedPCA; }
  };

Al construir varios modelos, todos los métodos analizados anteriormente para guardar los datos en los parámetros han recibido un identificador de archivo para escribir los datos. El método análogo de esta clase no supone una excepción. En el cuerpo del método, comprobaremos inmediatamente la validez del identificador recibido.

bool CPCA::Save(const int file_handle)
  {
   if(file_handle == INVALID_HANDLE)
      return false;

A continuación, guardaremos el valor de la bandera de entrenamiento del modelo. Después de todo, será su estado el que determine la necesidad de guardar el resto de los datos. Si el modelo aún no ha sido entrenado, no tendremos que guardar las matrices y vectores vacíos, simplemente finalizaremos el método.

   if(FileWriteInteger(file_handle, (int)b_Studied) < INT_VALUE)
      return false;
   if(!b_Studied)
      return true;

Si el modelo ha sido entrenado, procederemos a guardar los elementos restantes. Primero guardaremos la matriz de reducción. Para las matrices en el lenguaje MQL5, aún no se ha implementado la función de almacenamiento de datos, pero tenemos un método para escribir en el archivo del búfer de datos. ¿Por qué no aprovechar esto?

Primero, transferiremos los datos de la matriz al búfer de datos dinámico. Luego guardaremos el número de columnas de la matriz, y llamaremos al método save de nuestro búfer de datos. Aquí deberemos recordar que en el método de transferencia de datos de la matriz al búfer, no guardaremos el puntero al objeto. Como ya hemos indicado arriba, todo el trabajo de borrado de la memoria de dicho objeto recae en el programa que realiza la llamada. Por lo tanto, después de completar las operaciones de guardado de datos, deberemos eliminar el objeto creado.

   CBufferDouble *temp = FromMatrix(m_Ureduce);
   if(CheckPointer(temp) == POINTER_INVALID)
      return false;
   if(FileWriteLong(file_handle, (long)m_Ureduce.Cols()) <= 0)
     {
      delete temp;
      return false;
     }
   if(!temp.Save(file_handle))
     {
      delete temp;
      return false;
     }
   delete temp;

Usaremos un algoritmo similar para guardar estos vectores.

   temp = FromVector(v_Means);
   if(CheckPointer(temp) == POINTER_INVALID)
      return false;
   if(!temp.Save(file_handle))
     {
      delete temp;
      return false;
     }
   delete temp;

   temp = FromVector(v_STDs);
   if(CheckPointer(temp) == POINTER_INVALID)
      return false;
   if(!temp.Save(file_handle))
     {
      delete temp;
      return false;
     }
   delete temp;
//---
   return true;
  }

Después de completar con éxito todas las operaciones, saldremos del método con el resultado true.

La restauración de datos desde un archivo se realiza en el método Load, en el mismo orden. Primero, verificaremos la validez del identificador del archivo para cargar los datos.

bool CPCA::Load(const int file_handle)
  {
   if(file_handle == INVALID_HANDLE)
      return false;

Luego leeremos el estado de la bandera de aprendizaje del modelo, y si el modelo aún no ha sido entrenado, saldremos del método con un resultado positivo. No necesitaremos hacer ningún trabajo con la matriz de reducción y los vectores, ya que se reescribirán durante el entrenamiento del modelo. Y si tratamos de reducir la dimensionalidad de los datos antes del entrenamiento, se verificará el estado de la bandera de entrenamiento y el método se completará con un resultado negativo.

   b_Studied = (bool)FileReadInteger(file_handle);
   if(!b_Studied)
      return true;

Para el modelo entrenado, primero crearemos un objeto de búfer dinámico. Luego contaremos el número de columnas en la matriz de reducción, y cargaremos el contenido de la matriz de reducción en el búfer de datos.

Después de cargar con éxito los datos, simplemente transferiremos el contenido del búfer dinámico a nuestra matriz.

   CBufferDouble *temp = new CBufferDouble();
   if(CheckPointer(temp) == POINTER_INVALID)
      return false;
   long cols = FileReadLong(file_handle);
   if(!temp.Load(file_handle))
     {
      delete temp;
      return false;
     }
   m_Ureduce = FromBuffer(temp, cols);

Usando un algoritmo similar, cargaremos el contenido de los vectores.

   if(!temp.Load(file_handle))
     {
      delete temp;
      return false;
     }
   v_Means = FromBuffer(temp);

   if(!temp.Load(file_handle))
     {
      delete temp;
      return false;
     }
   v_STDs = FromBuffer(temp);

Después de cargar con éxito todos los datos, eliminaremos el objeto de búfer de datos dinámico y saldremos del método con un resultado positivo.

   delete temp;
//---
   return true;
  }

Con esto finalizará nuestro trabajo en nuestra clase de método para el Análisis de Componentes Principales. Podrá encontrar el código completo de todos los métodos y funciones en el archivo adjunto.


4. Simulación

Vamos a poner a prueba el funcionamiento de nuestra clase de método para el análisis de componentes principales en 2 etapas. En la primera prueba, entrenaremos al modelo. Para hacer esto, hemos creado el asesor experto "pca.mq5", basado en el asesor experto del artículo anterior "kmeans.mq5". Los cambios han afectado solo al objeto del modelo usado y a la función de entrenamiento del modelo Train.

Al comienzo del procedimiento, como antes, determinaremos la fecha de inicio del periodo de entrenamiento.

void Train(void)
  {
//---
   MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year -= StudyPeriod;
   if(start_time.year <= 0)
      start_time.year = 1900;
   datetime st_time = StructToTime(start_time);

 Luego descargaremos las cotizaciones y los valores de los indicadores usados.

   int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
      return;
   if(!ArraySetAsSeries(Rates, true))
      return;
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();

Después de ello, agruparemos los datos obtenidos en una matriz,  

   int total = bars - (int)HistoryBars;
   matrix data;
   if(!data.Init(total, 8 * HistoryBars))
     {
      ExpertRemove();
      return;
     }
//---
   for(int i = 0; i < total; i++)
     {
      Comment(StringFormat("Create data: %d of %d", i, total));
      for(int b = 0; b < (int)HistoryBars; b++)
        {
         int bar = i + b;
         int shift = b * 8;
         double open = Rates[bar]
                       .open;
         data[i, shift] = open - Rates[bar].low;
         data[i, shift + 1] = Rates[bar].high - open;
         data[i, shift + 2] = Rates[bar].close - open;
         data[i, shift + 3] = RSI.GetData(MAIN_LINE, bar);
         data[i, shift + 4] = CCI.GetData(MAIN_LINE, bar);
         data[i, shift + 5] = ATR.GetData(MAIN_LINE, bar);
         data[i, shift + 6] = MACD.GetData(MAIN_LINE, bar);
         data[i, shift + 7] = MACD.GetData(SIGNAL_LINE, bar);
        }
     }

llamando a continuación al método de entrenamiento de nuestro modelo.

   ResetLastError();
   if(!PCA.Study(data))
     {
      printf("Ошибка выполнения %d", GetLastError());
      return;
     }

Después de realizar con éxito el entrenamiento, guardaremos el modelo en un archivo y llamaremos al asesor experto para completar su trabajo.

   int handl = FileOpen("pca.net", FILE_WRITE | FILE_BIN);
   if(handl != INVALID_HANDLE)
     {
      PCA.Save(handl);
      FileClose(handl);
     }
//---
   Comment("");
   ExpertRemove();
  }

Podrá encontrar el código completo del asesor en el archivo adjunto.

Como resultado de la ejecución de este asesor experto usando los datos históricos de los últimos 15 años, hemos logrado reducir la dimensionalidad de los datos iniciales de 160 elementos a 68. Es decir, la reducción del tamaño de los datos originales es de casi 2,4 veces, con el riesgo de perder solo el 1% de la información.

En la siguiente etapa de prueba, tomaremos el modelo de análisis de componentes principales ya entrenado, y después de reducir la dimensionalidad de los datos iniciales, enviaremos el resultado del funcionamiento de nuestra clase a la entrada de un perceptrón completamente conectado. Para esta prueba, hemos creado el asesor experto "pca_net.mq5", basado en un asesor experto similar del artículo anterior, "kmeans_net.mq5". El entrenamiento del perceptrón se ha realizado usando los datos históricos de los últimos 2 años.

Resultados del entrenamiento del Perceptrón con datos comprimidos

Como podemos observar en el gráfico presentado, al entrenar el modelo usando datos comprimidos, existe una tendencia bastante estable a reducir el error. Después de 55 épocas de entrenamiento, el tamaño del error aún no se ha estabilizado. Esto significa que es posible lograr una mayor reducción de los errores con el entrenamiento continuo.


Conclusión

En este artículo, hemos analizado la solución de un tipo de problema más usando algoritmos de aprendizaje no supervisado: la reducción de la dimensionalidad. Para resolver estos problemas, hemos creado la clase CPCA, en la que se implementa el algoritmo del método de análisis de componentes principales. Este es un método de compresión de datos bastante eficiente, con un umbral de pérdida de información predecible.

Al probar la clase creada, hemos logrado comprimir los datos originales casi 2,4 veces, con un riesgo de pérdida de solo el 1% de la información, lo cual, como ve, es un resultado bastante bueno y nos permite aumentar la eficiencia de un modelo entrenado con datos comprimidos.

Además, una de las características del método de componentes principales es el uso de una matriz ortogonal para reducir la dimensionalidad, lo cual prácticamente reduce la correlación entre las características en los datos comprimidos a "0". Esta propiedad también aumenta la eficiencia del entrenamiento posterior del modelo con datos comprimidos, y esto se confirma con los resultados de la segunda prueba.

Al mismo tiempo, debemos advertir al lector contra el uso del método de componentes principales al intentar combatir el sobreajuste del modelo. Esta es una práctica bastante mala. En tales casos, será aún mejor usar métodos de regularización.

Y una observación más sobre la práctica general. A pesar de que en el proceso de compresión de datos se pierde una pequeña cantidad de información, esto sigue sucediendo. Por lo tanto, le recomendamos usar los métodos de reducción de la dimensionalidad solo si el entrenamiento de modelos sin su uso no ofrece los resultados esperados.

Asimismo, nos hemos familiarizado con las operaciones matriciales. Querríamos mostrar especial gratitud a MetaQuotes por la inclusión de estas en el lenguaje MQL5. El uso de operaciones matriciales simplifica enormemente la escritura de código a la hora de crear los modelos que estamos analizando para resolver problemas de inteligencia artificial.

Enlaces

  1. Redes neuronales: así de sencillo (Parte 14): Clusterización de datos
  2. Redes neuronales: así de sencillo (Parte 15): Clusterización de datos usando MQL5
  3. Redes neuronales: así de sencillo (Parte 16): Uso práctico de la clusterización

Programas utilizados en el artículo.

# Nombre Tipo Descripción
1 pca.mq5 Asesor   Asesor para el entrenamiento de modelos 
2 pca_net.mq5 Asesor
Asesor para probar la transmisión de datos al segundo modelo
3 pсa.mqh Biblioteca de clases
Biblioteca para organizar el método de análisis de componentes principales
4 kmeans.mqh  Biblioteca de clases Biblioteca para organizar el método de k-medias 
5 unsupervised.cl Biblioteca
Biblioteca de código del programa OpenCL para organizar el método de k-medias
6 NeuroNet.mqh Biblioteca de clases Biblioteca de clases para crear una red neuronal
7 NeuroNet.cl Biblioteca Biblioteca de código de programa OpenCL


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

Archivos adjuntos |
MQL5.zip (70.9 KB)
Trading de cuadrícula automatizado utilizando órdenes límite en la Bolsa de Moscú MOEX Trading de cuadrícula automatizado utilizando órdenes límite en la Bolsa de Moscú MOEX
Hoy vamos a desarrollar un asesor comercial en el lenguaje de estrategias comerciales MQL5 para MetaTrader 5 de la Bolsa de Moscú MOEX. El asesor comerciará con una estrategia de cuadrícula en el terminal MetaTrader 5 en los mercados de la Bolsa de Moscú MOEX; también incluirá el cierre de posiciones usando stop loss o take profit, y eliminará las órdenes pendientes al suceder ciertas condiciones del mercado.
Aprendizaje automático y data science (Parte 04): Predicción de una caída bursátil Aprendizaje automático y data science (Parte 04): Predicción de una caída bursátil
En este artículo, intentaremos usar nuestro modelo logístico para predecir una caída del mercado de valores según las principales acciones de la economía estadounidense: NETFLIX y APPLE. Analizaremos estas acciones, y también usaremos la información sobre las anteriores caídas del mercado en 2019 y 2020. Veamos cómo funcionará nuestro modelo en las poco favorables condiciones actuales.
DoEasy. Elementos de control (Parte 8): Objetos básicos WinForms por categorías, controles "GroupBox" y "CheckBox DoEasy. Elementos de control (Parte 8): Objetos básicos WinForms por categorías, controles "GroupBox" y "CheckBox
En este artículo, veremos la creación de los objetos WinForms "GroupBox" y "CheckBox", y crearemos los objetos básicos para las categorías de los objetos WinForms. Todos los objetos que hemos creado hasta ahora son estáticos, es decir, no tienen ninguna funcionalidad para interactuar con el ratón.
Aprendizaje automático y data science (Parte 03): Regresión matricial Aprendizaje automático y data science (Parte 03): Regresión matricial
En esta ocasión, vamos a crear modelos usando matrices: estas ofrecen una gran flexibilidad y permiten crear modelos potentes que pueden manejar no solo cinco variables independientes, sino muchas otras, tantas como los límites computacionales de nuestro ordenador nos permitan. El presente artículo será muy interesante, eso seguro.