English Русский 中文 Español Deutsch 日本語
preview
Redes neurais de maneira fácil (Parte 17): Redução de dimensionalidade

Redes neurais de maneira fácil (Parte 17): Redução de dimensionalidade

MetaTrader 5Sistemas de negociação | 30 agosto 2022, 09:22
493 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Conteúdo

Introdução

Continuamos a mergulhar no estudo de modelos e algoritmos de aprendizagem não supervisionado. Já vimos algoritmos de agrupamento de dados. E neste artigo quero que você saiba como resolver o problema de redução de dimensionalidade. Essencialmente, é feito por meio de certos algoritmos de compressão de dados que são amplamente utilizados na prática. Vamos conhecer a implementação de um desses algoritmos e a possibilidade de usá-lo na construção do nosso modelo de negociação.


1. Entendendo o problema de redução de dimensionalidade

Cada novo dia, cada nova hora e cada novo momento nos dá uma enorme quantidade de informações em todas as esferas da vida humana. Em nosso mundo das tecnologias da informação, as pessoas procuram salvar e processar o máximo possível dessas informações. E para armazenar grandes quantidades de informações, são necessários grandes armazéns de dados. Além disso, o processamento de grandes quantidades de dados requer recursos computacionais apropriados. E, naturalmente, há o desejo de encontrar uma maneira de registrar as informações disponíveis de forma mais concisa. No entanto, se a versão comprimida desses dados levar consigo o contexto completo da informação, serão necessários menos recursos para processá-la.

Por exemplo, imagine que estivéssemos reconhecendo padrões em uma imagem de 200*200 pixels e cada pixel fosse registrado em formato color, que ocupa 4 bytes na memória. Representar cada pixel em uma das 16,5 milhões de cores é claramente uma tarefa redundante. E na maioria dos casos, o desempenho do nosso modelo não será afetado se reduzirmos a gradação para, digamos, 16 ou 32 cores. Neste caso, para registrar o número de cor de cada pixel, 1 byte será suficiente. Claro, precisamos de um custo único para registrar nossa matriz de cores, 64 bytes para 16 cores e 128 bytes para 32 cores. Concordo, este não é um grande sacrifício a pagar por compactar todas as nossas imagens em 4 vezes. Se pensarmos nisso, problemas semelhantes também podem ser resolvidos com o método de agrupamento que já conhecemos. Embora esta não seja a maneira mais eficiente.

Outra área que usa as técnicas de redução de dimensionalidade é a visualização de dados. Por exemplo, você tem dados que descrevem os estados de um determinado sistema, representados por 10 parâmetros. E você precisa encontrar uma maneira de visualizar esses dados. Para a percepção humana, as imagens 2D e 3D são as preferidas. Claro, você pode fazer vários slides com diferentes variações de 2-3 parâmetros. Mas isso não dá uma imagem completa do estado do sistema. E, na maioria dos casos, diferentes estados em diferentes slides serão mesclados em um ponto. E nem sempre serão os mesmos estados.

Portanto, gostaríamos de encontrar um algoritmo que nos ajudasse a transferir todos os estados do nosso sistema de 10 parâmetros para o espaço de 2 ou 3 dimensões e que, ao mesmo tempo, nos permita dividir nossos estados do sistema o máximo possível, mantendo sua posição relativa. E, claro, tudo isso com perda mínima de informação.

Talvez você diga: "Isso tudo é interessante, mas qual é o uso prático ao negociar?" Vamos dar uma olhada no nosso terminal, quantos indicadores nos dá. Sim, muitos deles têm correlação de dados em algum grau. Mas cada um deles nos dá pelo menos um valor que descreve nossa situação de mercado. E se for multiplicado pelo número de instrumentos de negociação. Sendo que também diferentes variações de parâmetros de indicadores e de períodos gráficos analisados podem aumentar o número de parâmetros para descrever o estado atual do mercado indefinidamente.

Sem dúvida, não estudaremos imediatamente todos os instrumentos e todos os possíveis indicadores em um modelo. No entanto, podemos usar um número bastante grande deles, em busca da combinação mais adequada. E isso vai complicar nosso modelo e a duração do treinamento. Por isso, reduzir a dimensionalidade dos dados de entrada mantendo o máximo de informações é uma forma direta de reduzir o custo de treinamento do modelo e diminuir o tempo de tomada de decisão. Isso significa que a reação ao comportamento do mercado será muito rápida, e aos negócios serão feitos ao preço mais favorável.

E é necessário dizer também que os algoritmos de redução de dimensionalidade são sempre usados apenas para pré-processamento de dados. Uma vez que eles retornam apenas uma forma compactada dos dados de entrada, que são então armazenados ou transmitidos para processamento posterior. Isto pode ser a visualização ou o processamento de dados por algum outro modelo.

Assim, para construir nosso sistema de negociação, podemos obter o máximo de informações necessárias para descrever o estado atual do mercado. Podemos compactar essas informações usando um dos algoritmos de redução de dimensionalidade. E também esperamos que parte do ruído e dados correlacionados sejam eliminados durante o processo de compactação. E vamos alimentar com os dados compactados nosso modelo de tomada de decisão de trading.

Eu espero que a ideia seja clara. E para implementar isso, como algoritmo de redução de dimensionalidade, proponho adotar um dos métodos mais comuns de análise de componentes principais. Este algoritmo tem se mostrado em várias tarefas e pode ser estendido a novos dados. Isso permite compactar os dados recebidos e transferi-los para o modelo de tomada de decisão, gerando decisões de negociação em tempo real.

2. Método de análise de componentes principais (PCA)

A análise de componentes principais foi inventada pelo matemático inglês Karl Pearson em 1901. Desde então, tem sido usado com sucesso em muitos campos da ciência.

Para entender a essência do método em si, proponho a tarefa simplificada de reduzir a dimensão de uma matriz de dados bidimensional a um vetor. Do ponto de vista geométrico, isso pode ser representado como uma projeção de pontos de um determinado plano em uma linha reta.

Na figura abaixo, os dados de entrada são representados por pontos azuis, e duas projeções são feitas nas linhas laranja e cinza com pontos da cor correspondente. Como podemos ver, a distância média entre os pontos iniciais e suas projeções laranjas será menor do que as distâncias semelhantes às projeções cinzas. Neste caso, entre as projeções cinzas, nota-se a sobreposição das projeções dos pontos entre si. Portanto, a projeção laranja é o que nós estamos buscando, pois separa todos os pontos e tem menos perda de dados quando a dimensionalidade é reduzida (distância entre os pontos e suas projeções).

Tal linha é chamada de componente principal. Daí o nome do método - análise de componentes principais.

Do ponto de vista matemático, cada componente principal é um vetor numérico com tamanho igual à dimensionalidade dos dados de entrada. O produto do vetor de dados de entrada que descrevem um estado do sistema pelo vetor correspondente do componente principal fornece o ponto de projeção do estado analisado na linha reta.

Dependendo da dimensionalidade dos dados de entrada e dos requisitos para compactação de dados, pode haver vários desses componentes principais, mas não mais do que a dimensionalidade dos dados de entrada. Ao renderizar uma projeção volumétrica, eles serão 3. E a compressão de dados é baseada em uma margem de erro, geralmente levando uma perda de até 1% dos dados.

Método de componentes principais

Você provavelmente deve prestar atenção que isso é visualmente semelhante à regressão linear. Mas estes são métodos completamente diferentes e dão resultados diferentes.

Ao construir uma regressão linear, é exibida uma dependência linear do valor de uma variável em função de outra. E são minimizadas as distâncias à linha perpendicular aos eixos coordenados. Tal linha pode passar em qualquer parte do plano.

Na análise de componentes principais, os valores ao longo de todos os eixos são absolutamente independentes e equivalentes. As distâncias que são perpendiculares à própria linha, e não aos eixos coordenados, são minimizadas. E a linha do componente principal passa necessariamente pela origem. Portanto, todos os dados de entrada devem ser normalizados antes de aplicar o método, ou pelo menos centrado em torno da origem. Em outras palavras, precisamos centralizar os dados em relação a "0" em cada dimensão. 

Outra característica importante do método de análise de componentes principais é que, como resultado de sua aplicação, obtemos uma matriz de vetores ortogonais de componentes principais. Ou seja, não há correlação entre todos os vetores de componentes principais. Este fato impacta positivamente em todo o processo de aprendizagem do modelo de tomada de decisão ulterior, que recebe dados compactados como entrada.

Do ponto de vista matemático, o método de análise de componentes principais pode ser representado como uma decomposição espectral da matriz de covariância dos dados de entrada. E a matriz de covariância é fácil de encontrar pela fórmula:

Fórmula de matriz de covariância

Onde

  • C - matriz de covariância,
  • X - matriz de dados de entrada,
  • n - número de elementos nos dados de entrada.

Como resultado desta operação, obtemos uma matriz de covariância quadrada. O tamanho é igual ao número de características que descrevem o estado do sistema. A diagonal principal desta matriz será a variação das características. E os elementos restantes da matriz representam o grau de covariância dos pares de características correspondentes.

O próximo passo é realizar uma decomposição em valores singulares da matriz de covariância resultante. A própria decomposição em valores singulares da matriz é um processo matemático bastante complexo. Mas a introdução de matrizes e operações de matrizes na linguagem MQL5 simplifica muito esse processo para nós, uma vez que esta operação já é implementada para matrizes. Portanto, analisamos imediatamente os resultados da decomposição em valores singulares.

Decomposição em valores singulares de uma matriz

Como resultado da decomposição do valor singular da matriz, obtemos 3 matrizes, cujo produto é igual à matriz original. A matriz do meio ∑ é uma matriz diagonal com tamanho igual à matriz original. Ao longo da diagonal principal desta matriz encontram-se números singulares que representam a dispersão de valores ao longo dos eixos de vetores singulares. Os números singulares não são negativos e estão dispostos em ordem decrescente. Todos os outros elementos da matriz são iguais a "0". Portanto, muitas vezes ela é representada como um vetor.

U e V são matrizes quadradas unitárias contendo vetores singulares à esquerda e à direita, respectivamente. O tamanho da matriz U é igual ao número de linhas na matriz original, e o tamanho da matriz V é igual ao número de colunas na matriz original.

Em nosso caso particular, quando realizamos a decomposição em valor singular da matriz de covariância quadrada, a matriz U e V são do mesmo tamanho.

Com o objetivo de reduzir a dimensionalidade dos dados, utilizaremos uma matriz U. Como os valores singulares estão em ordem decrescente, basta pegar o número necessário de primeiras colunas da matriz U. Denotamos a nova matriz como a matriz UR. Para reduzir a dimensionalidade, basta multiplicar a matriz de dados de entrada pela matriz UR criada por nós.

Redução da dimensionalidade

Isto levanta a questão de qual é o valor ideal para reduzir a dimensionalidade. Se nos deparamos com a tarefa de visualizar dados, essa questão não surgirá. Pois a escolha da dimensionalidade de 1 a 3 depende da projeção a ser criada. Mas planejamos compactar os dados com perda mínima de informações para posterior transferência para outro modelo de decisão. Portanto, o principal critério para nós é a quantidade de informações gastas.

A melhor opção para determinar a quantidade de informações armazenadas é calcular a proporção de valores singulares correspondentes aos vetores singulares utilizados.

Proporção de informações transmitidas

Onde

  • k - número de vetores usados,
  • N - número total de valores singulares.

Na prática, o número de colunas k é geralmente escolhido de modo que o valor da razão acima seja de pelo menos 0,99. O que corresponde à preservação de 99% da informação.

Agora que já conhecemos os aspectos teóricos gerais, podemos prosseguir passar à implementação do método.


3. Implementação de PCA usando MQL5

Para implementar o algoritmo de análise de componentes principais, criaremos uma nova classe CPCA herdeira da classe base CObject. Salvaremos todo o código da nova classe no arquivo "pca.mqh".

Note que na implementação desta classe usaremos operações matriciais. Por isso, resultado do treinamento do nosso modelo, ou melhor, a matriz UR deverá ser salva na matrizm_Ureduce.

Além disso, vamos declarar mais 3 variáveis locais: o sinalizador de status de treinamento do modelo b_Studied e também os vetores v_Means ev_STDs, nos quais salvaremos os valores de médias aritméticas e desvios padrão para posterior normalização dos dados.

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

No construtor da classe, definimos o sinalizador de estado de treinamento do modelo b_Studied como false e inicializamos a matriz m_Ureduce com tamanho zero. Deixamos o destruidor de classe vazio, pois não criamos nenhum objeto aninhado dentro da classe.

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

Em seguida, recriamos o método de treinamento do modelo de Study. Nos parâmetros, o método recebe uma matriz de dados de entrada, e retorna o resultado lógico da operação.

Como mencionado acima, para realizar a análise de componentes principais, é necessário usar dados normalizados. Portanto, antes de dar início à implementação do algoritmo principal do método, normalizaremos os dados de entrada usando a fórmula abaixo. 

Normalização de dados

O uso de operações matriciais também simplifica esta tarefa para nós. Agora não precisamos criar sistemas de ciclos. Para encontrar os valores médios aritméticos para todas as características, basta usar o método Mean de operações matriciais com uma dimensão para calcular os valores. Como resultado da operação, obtemos imediatamente um vetor que contém o valor médio aritmético de todas as características.

No denominador da nossa fórmula de normalização de dados, vemos a raiz quadrada da variância, que corresponde ao desvio padrão. É aqui que as operações matriciais são úteis. O método STD retorna um vetor de desvios padrão para a dimensão especificada. Precisamos apenas adicionar uma pequena constante para eliminar o erro de divisão por zero.

Salvaremos os vetores resultantes nas variáveis correspondentes v_Means e v_STDs. Afinal, precisamos realizar uma normalização semelhante dos dados de entrada tanto na etapa de treinamento do modelo quanto na etapa de implementação prática.

Em seguida, normalizamos diretamente os dados. Para fazer isso, vamos preparar uma matriz X igual ao tamanho dos dados de entrada. E preparamos um loop com o número de iterações igual ao número de linhas na matriz de dados de entrada.

No corpo do loop, vamos normalizar os dados de entrada, e vamos armazenar o resultado das operações na matriz X criada anteriormente. O uso de operações vetoriais também nos ajudará a nos livrar da necessidade de criar um loop aninhado.

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);
     }

Após normalizar os dados de entrada, procede-se diretamente à implementação do algoritmo de análise de componentes principais. Como mencionado acima, na primeira etapa temos que calcular a matriz de covariância. Graças ao uso de operações matriciais, isso se encaixa facilmente em uma linha de código. Para não criar objetos desnecessários, sobrescrevo o resultado das operações em nossa matriz X.

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

De acordo com o algoritmo acima, a próxima operação deve ser a decomposição em valores singulares da matriz de covariâncias. Como resultado desta operação, esperamos obter 3 matrizes: vetores singulares à esquerda, valores singulares e vetores singulares à direita. Como você lembra, os elementos da matriz de valores singulares apenas ao longo da diagonal principal podem ter valores diferentes de zero. Portanto, para economizar recursos na implementação do MQL5, um vetor de valores singulares é retornado ao invés de uma matriz.

Antes de chamar a função, vamos declarar 2 matrizes e 1 vetor para obter os resultados. E só então chamamos o método matricial de decomposição em valor singular SVD. Nos parâmetros do método, passaremos matrizes e um vetor para registro dos resultados da operação.

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

Agora que recebemos matrizes ortogonais de vetores singulares, precisamos determinar em que nível reduziremos a dimensionalidade dos dados de entrada. Como prática geral, reteremos pelo menos 99% das informações contidas nos dados de entrada.

Seguindo a lógica acima, primeiro determinamos a soma total de todos os elementos do vetor de valores singulares. Além disso, verificamos se o valor resultante é maior que “0”. Por definição, não pode ser negativo, pois os números singulares não são negativos. Além disso, devemos excluir o erro de divisão por "0".

Depois disso, calculamos as somas acumuladas dos valores do vetor de valores singulares e dividimos o vetor resultante pela soma total dos valores singulares.

Como resultado, obteremos um vetor de valores crescentes com máximo igual a "1".

Agora, para determinar o número de colunas necessárias, resta-nos encontrar a posição do primeiro elemento no vetor que é maior ou igual ao valor limite para armazenar informações. No exemplo mostrado, o valor é 0,99. Isso garante a preservação de 99% da informação 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++;

Basta redimensionar a matriz e transferir seu conteúdo para a matriz da nossa classe. Depois disso, alteramos o sinalizador de treinamento do modelo e saímos do método.

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

Depois criamos um método para treinar o modelo, ou melhor, determinar a matriz de redução de dimensionalidade dos dados de entrada. Também podemos criar um método ReduceM para reduzir os dados recebidos. Esse método recebe os dados de entrada nos parâmetros e retorna uma matriz de dimensionalidade reduzida.

Obviamente, os dados de entrada devem ser comparáveis aos dados usados para treinar o modelo. Aqui estamos falando da quantidade e qualidade dos sinais que descrevem o estado do sistema, e não do número de observações.

No início do método, criamos um bloco de controles, no qual verificamos o sinalizador de treinamento do modelo e a igualdade entre o número de colunas na matriz de dados de entrada (número de recursos) e o número de linhas em nossa matriz de redução m_Ureduce. Se alguma das condições não for atendida, saímos do método e retornamos uma matriz de tamanho zero.

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

Depois de passar com sucesso o bloco de controles, precisamos normalizar os dados de entrada antes de realizar a redução de dimensionalidade. O algoritmo de normalização será semelhante ao discutido acima ao treinar o modelo. Só que desta vez não vamos calcular a média aritmética e o desvio padrão, e usaremos os vetores correspondentes salvos durante o treinamento. Assim, garantiremos que os novos resultados e os obtidos durante o treinamento sejam comparáveis.

   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 da conclusão do algoritmo do método, resta-nos multiplicar a matriz de valores normalizados pela matriz de redução e retornar o resultado da operação ao programa de chamada.

   return result.MatMul(m_Ureduce);
  }

Assim, construímos métodos para treinar o modelo de redução da dimensionalidade dos dados de entrada. Graças ao uso de operações matriciais, o código ficou bastante conciso e sem mergulhar em sutilezas matemáticas. Mas lembremos que este é o primeiro código em nossa biblioteca usando operações matriciais. E antes disso, usávamos matrizes dinâmicas em objetos CBufferDouble. Portanto, para elaborar a compatibilidade de nossos objetos, é necessário criar uma interface para transferência de dados de um buffer dinâmico para uma matriz e vice-versa.

Para gerar este processo, vamos criar os métodosFromBuffer e FromMatrix. O primeiro método terá como parâmetros um buffer de dados dinâmico e o tamanho de um vetor que descreve um estado do sistema. E retornará uma matriz para a qual o conteúdo do buffer será transferido.

No corpo do método, primeiro criamos um bloco de controles. Nele primeiro verificamos a validade do ponteiro para o objeto de buffer de dados de entrada. E, em seguida, verificamos a multiplicidade do tamanho do buffer pelo tamanho do vetor de descrição de um estado do sistema analisado.

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;
     }

Depois de passar com sucesso em todas as verificações, determinamos o número de linhas na matriz e inicializamos a matriz de resultados.

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

Em seguida, preparamos um sistema de loops aninhados, no qual transferimos todo o conteúdo do buffer dinâmico para a 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;
  }

Após a conclusão do sistema de loops, saímos do método e retornamos a matriz criada para o programa de chamada.

O segundo método FromMatrix efetua a operação inversa. Nos parâmetros do método, passamos uma matriz com dados e, na saída, obtemos um buffer de dados dinâmico.

No corpo do método, primeiro criamos um novo objeto de matriz dinâmica e verificamos imediatamente o resultado da operação.

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

Em seguida, reservamos um tamanho da matriz dinâmica suficiente para armazenar todo o conteúdo da mesma.

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

Em seguida, basta transferir o conteúdo da matriz para uma matriz dinâmica. Esta operação é realizada em um sistema de dois loops aninhados.

   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;
  }

Depois que todas as operações do loop forem concluídas com êxito, saímos do método e retornamos o objeto de buffer de dados criado para o programa de chamada.

Como não armazenamos um ponteiro para o objeto criado, todo o trabalho de acompanhar seu estado e removê-lo da memória após a conclusão do trabalho deve ser elaborado no lado do programa de chamada.

Vamos criar métodos semelhantes para trabalhar com vetores. Vamos transferir dados do buffer para o vetor com o método FromBuffer sobrecarregado. E realizamos a operação inversa no método FromVector. Os algoritmos para a construção dos métodos são semelhantes aos apresentados acima. O código completo do script pode ser encontrado no anexo.

Após criar os métodos de transferência de dados, podemos criar uma sobrecarga do método de treinamento do modelo, que nos parâmetros receberá um buffer de dados dinâmico e o tamanho do vetor de descrição de um estado do sistema. O algoritmo para construir tal método é bastante simples. Primeiro transferimos os dados do buffer dinâmico para a matriz usando o método FromBuffer discutido acima. E então chamamos o método considerado anteriormente para treinar o modelo, passando a matriz resultante para ele.

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

Vamos criar uma sobrecarga semelhante para o método de redução de dimensionalidade ReduceM. Apenas em contraste com a sobrecarga considerada do método de aprendizado, nos parâmetros deste método passamos apenas o buffer dos dados de entrada sem especificar o tamanho do vetor de descrição de estado do sistema analisado. Isso se deve ao fato de que no momento o modelo já deve estar treinado e o tamanho do vetor de descrição do estado do sistema deve ser igual ao número de linhas da matriz de redução.

Outra diferença deste método é que, para evitar a transferência excessiva de dados, primeiro verificamos se o modelo está treinado e a multiplicidade do tamanho do buffer para o tamanho do vetor de descrição do estado do sistema. E somente depois de passar os controles com sucesso, chamamos o método de transferência de dados.

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 obter uma matriz de dimensionalidade reduzida na forma de um buffer de dados dinâmico, criaremos mais 2 métodos Reduce sobrecarregados. Um dos parâmetros receberá o buffer de dados dinâmico dos dados de entrada. A segunda é a matriz. O código é dado abaixo. 

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

Pode parecer estranho, mas apesar da diferença nos parâmetros dos métodos, seus conteúdos são exatamente os mesmos. Isso é facilmente explicado usando as sobrecargas do método ReduceM descritas acima.

Tratamos da funcionalidade da classe. Agora só temos que criar métodos para trabalhar com arquivos. Lembramos que qualquer modelo, uma vez treinado, deve ser capaz de restaurar rapidamente seu trabalho para ser usado posteriormente. E, como sempre, começaremos com o método Save.

Mas antes de dar início à construção do algoritmo do método de salvamento de dados, vamos analisar a estrutura de nossa classe e pensar no que devemos salvar em um arquivo.

Entre as variáveis de classe privada, vemos um sinalizador de treinamento de modelo b_Studied, uma matriz de redução m_Ureduce e 2 vetores com médias aritméticas v_Means e desvio padrão v_STDs. E para restaurar totalmente o desempenho do modelo, precisamos salvar todos esses 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; }
  };

Ao construir vários modelos, todos os métodos considerados anteriormente para salvar dados em parâmetros receberam um identificador de arquivo para gravar dados. O método semelhante desta classe não é exceção. E no corpo do método, verificamos imediatamente a validade do handle recebido.

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

Em seguida, salvamos o valor do sinalizador de treinamento do modelo. Afinal, é o seu estado que determina a necessidade de salvar o restante dos dados. Se o modelo ainda não foi treinado, não há necessidade de salvar vetores e matrizes vazios. Estamos simplesmente completando o trabalho do método.

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

Se o modelo for treinado, procedemos a salvar os elementos restantes. Primeiro salvamos a matriz de redução. Para matrizes na linguagem MQL5, a função de salvamento de dados ainda não foi implementada. Mas temos um método para gravar no arquivo de buffer de dados. Por que não aproveitamos isso?

Primeiro, vamos transferir os dados da matriz para o buffer de dados dinâmicos. Em seguida, salvamos o número de colunas da matriz. E vamos chamar o método de salvamento do nosso buffer de dados. E aqui devemos lembrar que no método de transferência de dados da matriz para o buffer, não salvamos o ponteiro para o objeto. E acima, já indiquei que todo o trabalho de limpar a memória de tal objeto está no programa de chamada. Portanto, após a conclusão das operações de salvamento de dados, devemos excluir o objeto criado.

   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 um algoritmo semelhante para salvar esses vetores.

   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;
  }

Após a conclusão bem-sucedida de todas as operações, saímos do método com o resultado true.

A restauração de dados desde um arquivo é realizada no método Load na mesma ordem. Primeiro, verificamos a validade do identificador de arquivo para carregar os dados.

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

Em seguida, lemos o estado do sinalizador de aprendizado do modelo. E se o modelo ainda não foi treinado, saímos do método com um resultado positivo. Não precisamos fazer nenhum trabalho com a matriz de redução e vetores, pois eles serão reescritos durante o treinamento do modelo. E quando você tentar realizar a redução da dimensionalidade dos dados antes do treinamento, o estado do sinalizador de treinamento será verificado e o método será concluído com um resultado negativo.

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

Para o modelo treinado, primeiro criaremos um objeto de buffer dinâmico. Em seguida, contamos o número de colunas na matriz de redução. E carregamos o conteúdo da matriz de redução no buffer de dados.

Após carregar os dados com sucesso, simplesmente transferimos o conteúdo do buffer dinâmico para nossa 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 um algoritmo semelhante, carregaremos o conteúdo dos vetores.

   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);

Depois de carregar todos os dados com sucesso, excluímos o objeto de buffer de dados dinâmicos e saímos do método com um resultado positivo.

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

Isso conclui nosso trabalho em nossa classe de método de análise de componentes principais. O código completo de todos os métodos e funções pode ser encontrado no anexo.


4. Teste

Testei o trabalho de nossa classe do método de análise de componentes principais em 2 etapas. No primeiro teste, treinei o modelo. Para fazer isso, criei o Expert Advisor "pca.mq5" baseado no Expert Advisor do artigo anterior "kmeans.mq5". As alterações afetaram apenas o objeto do modelo utilizado e a função de treinamento do modelo Train.

No início do procedimento, como antes, determinaremos a data de início do período de treinamento.

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);

 Em seguida, faremos o carregamento das cotações e dos valores dos indicadores utilizados.

   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();

Depois disso, agruparemos os dados recebidos em uma 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);
        }
     }

E chamaremos o método de treinamento do nosso modelo.

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

Após uma aprendizagem bem sucedida, salvaremos o modelo em um arquivo e chamaremos e encerraremos a EA.

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

O código completo do EA pode ser encontrado no anexo.

Como resultado da execução deste Expert Advisor usando dados históricos dos últimos 15 anos, foi possível reduzir a dimensionalidade dos dados de entrada de 160 elementos para 68. Ou seja, uma redução no tamanho dos dados de entrada em quase 2,4 vezes com o risco de perder apenas 1% das informações.

Na próxima etapa de teste, pegaremos o modelo de análise de componentes principais já treinado. E após reduzir a dimensionalidade dos dados de entrada, introduziremos o resultado do trabalho de nossa classe na entrada de um perceptron totalmente conectado. Para este teste, criamos o Expert Advisor "pca_net.mq5" baseado em um Expert Advisor semelhante do artigo anterior "kmeans_net.mq5". O treinamento do perceptron foi realizado com dados históricos dos últimos 2 anos.

Resultados do treinamento do perceptron usando dados compactados

Como pode ser visto no gráfico apresentado, ao treinar o modelo com dados compactados, há uma tendência bastante estável de redução do erro. Após 55 épocas de treinamento, o tamanho do erro ainda não se estabilizou. Isso significa que é possível reduzir ainda mais os erros com o treinamento contínuo.


Considerações finais

Neste artigo, vimos como podemos, com ajuda de algoritmos de aprendizado não supervisionado, resolver outro tipo de problema, especificamente a redução de dimensionalidade. Para resolver tais problemas, criamos a classe CPCA, que implementa o algoritmo de análise de componentes principais. Este é um método de compactação de dados bastante eficiente com um limite de perda de informações previsível.

Ao testar a classe criada, conseguimos compactar os dados de entrada em quase 2,4 vezes com o risco de perder apenas 1% das informações. O que, você vê, é um resultado muito bom e permite aumentar a eficiência de um modelo treinado com dados compactados.

Além disso, uma das características do método de componentes principais é a utilização de uma matriz ortogonal para redução de dimensionalidade. O que praticamente reduz a correlação entre características em dados compactados para "0". Essa propriedade também aumenta a eficiência do treinamento subsequente do modelo em dados compactados. E isso é confirmado pelos resultados do segundo teste.

Ao mesmo tempo, o leitor deve ser advertido contra o uso do método dos componentes principais para evitar o superajuste do modelo. Essa é uma prática muito ruim. Nesses casos, o melhor é usar métodos de regularização.

E outra observação extraída da prática geral. Apesar do fato de que, no processo de compactação de dados, uma pequena quantidade de informações é perdida, isso inda acontece. Desse modo, o uso de métodos de redução de dimensionalidade é recomendado apenas se os modelos de treinamento não derem os resultados esperados.

Além disso, nos familiarizamos com as operações matriciais. Agradecimentos especiais à MetaQuotes por introduzi-las na linguagem MQL5. O uso de operações matriciais simplifica muito a escrita de código ao criar os modelos que estamos considerando para resolver problemas de inteligência artificial.

Referências

  1. Redes neurais de maneira fácil (Parte 14): agrupamento de dados
  2. Redes neurais de maneira fácil (Parte 15): agrupamento de dados via MQL5
  3. Redes neurais de maneira fácil (Parte 16): Uso prático do agrupamento

Programas utilizados no artigo

# Nome Tipo Descrição
1 pca.mq5 EA   EA para treinamento de modelos 
2 pca_net.mq5 EA
EA de teste de transferência de dados de segundo modelo
3 pсa.mqh Biblioteca de classe
Biblioteca para preparar o método de análise de componentes principais
4 kmeans.mqh  Biblioteca de classe Biblioteca para preparar o método k-médias 
5 unsupervised.cl Biblioteca
Biblioteca de códigos OpenCL para preparar o método k-médias
6 NeuroNet.mqh Biblioteca de classe Biblioteca de classes para a criação de uma rede neural
7 NeuroNet.cl Biblioteca Biblioteca de códigos do programa OpenCL


Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/11032

Arquivos anexados |
MQL5.zip (70.9 KB)
DoEasy. Controles (Parte 8): Objetos básicos do WinForms por categoria, controles GroupBox e CheckBox DoEasy. Controles (Parte 8): Objetos básicos do WinForms por categoria, controles GroupBox e CheckBox
Neste artigo, veremos como criar dois objetos WinForms, especificamente GroupBox e CheckBox, e também geraremos objetos básicos para categorias de objetos WinForms. Todos os objetos criados ainda são estáticos, ou seja, não possuem a funcionalidade de interação com o mouse.
Negociação usando uma grade com ordens limitadas no MOEX Negociação usando uma grade com ordens limitadas no MOEX
Desenvolvimento de um Expert Advisor na linguagem de estratégias de negociação MQL5 para MetaTrader 5. Esse EA irá negociar no MOEX (Bolsa de Valores de Moscou), usando o terminal MetaTrader 5 e uma estratégia de grade, que incluirá o fechamento de posição por stop loss ou take profit, exclusão de ordens pendentes quando atendidas certas condições de mercado.
Desenvolvendo um EA de negociação do zero (Parte 29): Plataforma falante Desenvolvendo um EA de negociação do zero (Parte 29): Plataforma falante
Neste artigo vamos aprender como fazer a plataforma MT5 falar. Que tal deixar o EA mais divertido? Operar mercados financeiros costuma ser uma atividade extremamente chata e monótona, mas podemos deixar as coisas um pouco menos monótonas, apesar de que isto pode ser perigoso caso você tenha algum problema que lhe faça ficar viciado, pode ser que a coisa fique um pouco menos chata.
Como melhorar em aprendizado de máquina Como melhorar em aprendizado de máquina
Esta é uma seleção de materiais que será útil para que os operadores possam melhorar seus conhecimentos sobre negociação algorítmica. Os algoritmos simples são coisa do passado, agora é difícil alcançar o sucesso sem o uso de aprendizado de máquina e redes neurais.