
Informação mútua como critério para seleção progressiva de características
Introdução
A informação mútua é uma ferramenta valiosa para identificar preditores eficazes, especialmente quando se trata de relações complexas e não lineares. Ela pode revelar dependências que outros métodos podem deixar passar, tornando-se especialmente adequada para modelos capazes de explorar tais complexidades. Neste artigo é analisada a aplicação da informação mútua na escolha de características, com foco principal no algoritmo proposto por Hanchuan Peng, Fuhui Long e Chris Ding em seu trabalho de pesquisa intitulado "Seleção de características baseada em informação mútua: Critérios de máxima dependência, máxima relevância e mínima redundância" ("Feature Selection Based on Mutual Information: Criteria of Max-Dependency, Max-Relevance, and Min-Redundancy".
Começamos discutindo a avaliação da informação mútua para variáveis contínuas e, em seguida, avançamos para o processo de seleção de características em si. Por fim, ilustramos a eficácia do algoritmo com exemplos que utilizam tanto conjuntos de dados sintéticos quanto reais.
Avaliação da informação mútua de variáveis contínuas
A informação mútua, indicada como I(X;Y), quantifica a informação compartilhada entre duas variáveis aleatórias, X e Y. Para variáveis discretas, seu cálculo é relativamente simples e envolve a soma das probabilidades conjuntas e marginais.
No entanto, as variáveis contínuas representam um grande desafio devido à sua faixa infinita de valores possíveis. Uma abordagem comum para resolver esse problema é discretizá-las em intervalos e, então, aplicar a fórmula da informação mútua discreta. K infelizmente, a precisão desse método depende significativamente da largura ou do número de intervalos escolhidos, podendo levar a diferenças consideráveis no valor calculado da informação mútua.
Para calcular a informação mútua de variáveis contínuas, precisamos passar das somas discretas para integrais contínuas. Isso exige conhecimento das funções de densidade de probabilidade conjunta e marginal (PDF), que muitas vezes não estão disponíveis devido à limitação dos dados. O método da janela de Parzen oferece uma solução ao aproximar a PDF diretamente a partir dos dados. Esse método consiste em posicionar uma função de janela sobre os dados e calcular a soma ponderada dos pontos contidos dentro dessa janela. O peso atribuído a cada ponto diminui à medida que sua distância em relação ao centro da janela aumenta. Movendo essa janela pelo espaço dos dados e calculando a soma ponderada em cada ponto, podemos construir uma estimativa da função de densidade de probabilidade.
Esse método é especialmente útil em cenários em que a distribuição subjacente é desconhecida ou complexa. Ajustando a forma e o tamanho da janela, é possível aumentar a suavidade e a precisão da estimativa de densidade. No entanto, é importante destacar que a escolha dos parâmetros da janela pode afetar significativamente a qualidade da estimativa. A eficiência da estimativa de densidade de Parzen depende da escolha de uma função de peso adequada, que idealmente deve ser unitária e apresentar rápido decaimento conforme o argumento se afasta de zero. Uma escolha popular para essa função de peso é a função Gaussiana, caracterizada pelo parâmetro de escala sigma.
Entretanto, determinar o valor ótimo de sigma é uma tarefa complexa, frequentemente resultando em grandes variações na densidade calculada. Essa sensibilidade à escolha de sigma é a principal desvantagem do método da janela de Parzen, tornando-o pouco ideal para tarefas como a avaliação de preditores candidatos, nas quais uma estimativa precisa da densidade é indispensável.
Uma alternativa ao método da janela de Parzen para estimar a informação mútua entre variáveis contínuas é a partição adaptativa. A partição adaptativa representa um avanço significativo em relação à abordagem ingênua de divisão fixa em intervalos. Por meio da divisão iterativa de regiões com alto conteúdo de informação, a partição adaptativa concentra de forma eficiente os recursos computacionais nas áreas mais relevantes do espaço dos dados. Esse abordagem direcionada resulta em estimativas mais precisas da informação mútua, principalmente quando se trabalha com distribuições complexas e irregulares. A principal vantagem da partição adaptativa está em sua capacidade de se ajustar à estrutura subjacente dos dados. O processo recursivo de divisão garante que regiões com dependências significativas sejam subdivididas, enquanto regiões com pouca informação permaneçam intactas. Esse método dinâmico permite evitar os erros comuns da divisão fixa, que podem levar a excesso de suavização ou agrupamento indevido dos dados.
Além disso, a partição adaptativa incorpora testes estatísticos para controlar o processo de divisão. Ao avaliar a significância estatística das subdivisões potenciais, a partição adaptativa permite distinguir informação verdadeira de ruído aleatório. Isso ajuda a evitar o sobreajuste, que de outra forma poderia levar a estimativas infladas da informação mútua. O principal problema do método ingênuo é que ele dá atenção excessiva a regiões do domínio bivariado com poucos ou nenhum caso, ao mesmo tempo em que pode negligenciar áreas densas onde se concentra a maior parte da informação.
A partição adaptativa começa com uma divisão de baixa granularidade do espaço dos dados. Para cada partição é calculada a informação mútua entre as variáveis. Se esse valor excede um limite pré-definido, a partição é subdividida em seções menores. Esse processo recursivo de divisão continua até que seja atingido um critério de parada. Se o processo de divisão for interrompido cedo demais, as partições resultantes podem ser muito grosseiras, causando perda de detalhes e viés para baixo na estimativa da informação mútua. Por outro lado, se a divisão prosseguir por muito tempo, as partições podem se tornar excessivamente pequenas, resultando em sobreajuste e estimativas infladas da informação mútua devido ao ruído.
Para determinar se uma partição deve ou não ser subdividida, utiliza-se o teste do qui-quadrado. Esse teste avalia a independência entre duas variáveis dentro da partição. A partição é dividida em quatro subpartições, formando uma tabela de contingência 2x2. Conta-se o número de pontos de dados que caem em cada uma das quatro subdivisões. Sob a hipótese nula de independência entre as duas variáveis, o número esperado de pontos em cada subdivisão é calculado com base nos totais marginais. Em seguida, a estatística qui-quadrado obtida é comparada com o valor crítico da distribuição qui-quadrado com 1 grau de liberdade. Se o valor calculado excede o valor crítico, a hipótese nula de independência é rejeitada, indicando uma relação significativa entre as duas variáveis dentro da partição.
Se o teste do qui-quadrado for significativo, a partição é subdividida em regiões menores. Esse processo continua de forma recursiva até que as partições sejam suficientemente homogêneas ou até que seja satisfeito o critério de parada. Se o teste do qui-quadrado não for significativo, a partição é considerada homogênea e não há necessidade de novas subdivisões.
Para lidar com a possibilidade de surgirem relações mais complexas dentro da partição, que poderiam ser ignoradas em uma simples divisão 2x2, o algoritmo inclui uma verificação adicional. Se o teste qui-quadrado 2x2 não indicar relação significativa, mas a partição ainda for relativamente grande, é feita uma divisão mais detalhada 4x4. Em seguida, o teste qui-quadrado é aplicado a essa divisão 4x4 para avaliar a presença de uma distribuição não aleatória. Se o teste 4x4 também não identificar dependência significativa, a partição é considerada homogênea e seu valor é incorporado ao cálculo da informação mútua total. Nesse caso, não é necessário prosseguir com novas divisões.
Se o teste qui-quadrado 2x2 ou 4x4 apontar uma distribuição desigual dentro da partição, esta é dividida em quatro subdivisões menores. Essas subdivisões são tratadas de forma diferenciada conforme seu tamanho. Se uma subdivisão for muito pequena, ela é considerada final. Seu valor é calculado e somado à informação mútua total, e a subdivisão deixa de ser analisada para futuras divisões. Caso contrário, se a subdivisão for suficientemente grande, ela é marcada como candidata para novas divisões posteriores.
A estatística padrão do teste qui-quadrado dois por dois é apresentada na seguinte equação.
Partição adaptativa em MQL5
A classe Capm em mutualinfo.mqh implementa o algoritmo de partição adaptativa para estimar a informação mútua de variáveis contínuas. Para controlar o processo recursivo de divisão, ela utiliza a estrutura personalizada IntStack. Essa estrutura armazena informações sobre as partições e quaisquer subdivisões nelas contidas. A estrutura é composta por seis membros, descritos a seguir:
- Xstart e Xstop: Definem o intervalo de índices de uma variável dentro da partição atual.
- Ystart e Ystop: Definem o intervalo de índices da segunda variável dentro da partição atual.
- DataStart e DataStop: Determinam os índices inicial e final dos pontos de dados contidos na partição, delimitando as subdivisões.
//+------------------------------------------------------------------+ //| int type stack structure | //+------------------------------------------------------------------+ struct IntStack { int Xstart ; // X value (rank) at which this rectangle starts int Xstop ; // And stops int Ystart ; // Ditto for Y int Ystop ; int DataStart ; // Starting index into indices for the cases in this int DataStop ; // rectangle, and the (inclusive) ending index };
O construtor Capm recebe três argumentos:
- no_split: Um sinalizador lógico que define como tratar valores aproximadamente iguais. Se definido como true, o algoritmo garante que esses valores sejam agrupados durante a divisão, evitando que sejam separados em diferentes partições.
- dep_vals: Um vetor que contém os valores de uma das variáveis analisadas. Normalmente, essa é a variável cuja informação mútua será calculada em relação a um conjunto de outras variáveis.
- chi_critical_value: O valor limite usado nos testes qui-quadrado aplicados durante o processo de divisão. Esse valor define o nível de significância dos testes e influencia a profundidade da divisão. Em geral, valores entre 4,0 e 8,0 são adequados, sendo 6,0 uma escolha comum com utilidade geral.
//+------------------------------------------------------------------+ //| constructor | //+------------------------------------------------------------------+ Capm::Capm(bool no_split,vector &dep_vals, double chi_critical_value = 6.0) { m_no_split = no_split; m_size = int(dep_vals.Size()) ; m_chi_crit = chi_critical_value; if(ArrayResize(m_indices,m_size)<0 || !m_tempvals.Resize(m_size) || ArrayResize(m_ranks,m_size)<0 || (m_no_split && ArrayResize(m_tied_ranks,m_size)<0)) { Print(__FUNCTION__, " Arrayresize error ", GetLastError()); return; } for(int i=0 ; i<m_size ; i++) { m_tempvals[i] = dep_vals[i] ; m_indices[i] = i ; } qsortdsi(0, m_size-1, m_tempvals, m_indices) ; for(int i=0 ; i<m_size ; i++) { m_ranks[m_indices[i]] = i ; if(! m_no_split) continue ; if(i < m_size-1 && m_tempvals[i+1] - m_tempvals[i] < 1.e-12 * (1.0+fabs(m_tempvals[i])+fabs(m_tempvals[i+1]))) m_tied_ranks[i] = 1 ; else m_tied_ranks[i] = 0 ; } }
O construtor da classe Capm começa configurando o sinalizador no_split, que determina se devem ser consideradas as ligações nos dados, e o parâmetro chi_critical_value, que define o valor de corte para o teste qui-quadrado utilizado na detecção de dependências significativas entre as variáveis. Em seguida, o construtor aloca memória para os arrays de armazenamento de dados, ranks e valores associados. Ele copia os valores das variáveis para um vetor temporário e os ordena. Os índices originais dos pontos de dados são armazenados no array m_indices. Por fim, o construtor atribui ranks aos pontos de dados ordenados e identifica quaisquer valores ligados, marcando-os no array m_tied_ranks caso o sinalizador no_split esteja ativo.
//+------------------------------------------------------------------+ //| Mutual information using the Adaptive partitioning method | //+------------------------------------------------------------------+ class Capm { public: Capm(bool no_split,vector &dep_vals, double chi_critical_value = 6.0) ; ~Capm(void) ; double fit(vector &raw) ; private: bool m_no_split; // respect ties int m_size ; // Number of cases int m_ranks[] ; // 'Dependent' variable ranks int m_tied_ranks[] ; // tied[i] != 0 if case with rank i == case with rank i+1 double m_chi_crit ; // Chi-square test criterion int m_indices[]; // indices vector m_tempvals; // temp values } ;
O método fit() da classe Capm é projetado para estimar a informação mútua entre uma variável (fornecida ao construtor) e outra variável passada como argumento. Em seguida, recebe um vetor que representa os valores da segunda variável. Esse vetor deve ter o mesmo tamanho do vetor fornecido ao construtor. Chamando o método fit() várias vezes com vetores diferentes, é possível calcular de forma eficiente a informação mútua entre uma variável e diversas outras.
//+------------------------------------------------------------------+ //| mutual information for continuous variables | //+------------------------------------------------------------------+ double Capm::fit(vector &raw) { int i, k, ix, iy, nstack, splittable ; int current_indices[], x[], fullXstart, fullXstop, fullYstart, fullYstop, ipos ; int trialXstart[4], trialXstop[4], trialYstart[4], trialYstop[4] ; int ipx, ipy, xcut[4], ycut[4], iSubRec, x_tied[], ioff ; int X_AllTied, Y_AllTied ; int centerX, centerY, currentDataStart, currentDataStop ; int actual[4], actual44[16] ; vector expected(16), xfrac(4), yfrac(4) ; double diff, testval; double px, py, pxy, MI ; IntStack stack[]; if(ArrayResize(stack,1,256)<0) { Print(__FUNCTION__," arrayresize error ", GetLastError()); return EMPTY_VALUE; } if(ArrayResize(current_indices,m_size)<0 || ArrayResize(x,m_size)<0 || (m_no_split && ArrayResize(x_tied,m_size)<0)) { Print(__FUNCTION__, " Arrayresize error ", GetLastError()); return EMPTY_VALUE; } for(i=0 ; i<m_size ; i++) { m_tempvals[i] = raw[i] ; m_indices[i] = i ; } if(!qsortdsi(0, m_size-1, m_tempvals, m_indices)) { Print(__FUNCTION__, " Arrayresize error ", GetLastError()); return EMPTY_VALUE; } for(i=0 ; i<m_size ; i++) { x[m_indices[i]] = i ; if(! m_no_split) continue ; if(i < m_size-1 && m_tempvals[i+1] - m_tempvals[i] < 1.e-12 * (1.0+fabs(m_tempvals[i])+fabs(m_tempvals[i+1]))) x_tied[i] = 1 ; else x_tied[i] = 0 ; } for(i=0 ; i<m_size ; i++) { m_indices[i] = i ; } stack[0].Xstart = 0 ; stack[0].Xstop = m_size-1 ; stack[0].Ystart = 0 ; stack[0].Ystop = m_size-1 ; stack[0].DataStart = 0 ; stack[0].DataStop = m_size-1 ; nstack = 1 ; MI = 0.0 ; while(nstack > 0) { --nstack ; fullXstart = stack[nstack].Xstart ; fullXstop = stack[nstack].Xstop ; fullYstart = stack[nstack].Ystart ; fullYstop = stack[nstack].Ystop ; currentDataStart = stack[nstack].DataStart ; currentDataStop = stack[nstack].DataStop ; centerX = (fullXstart + fullXstop) / 2 ; X_AllTied = (x_tied.Size()) && (x_tied[centerX] != 0) ; if(X_AllTied) { for(ioff=1 ; centerX-ioff >= fullXstart ; ioff++) { if(! x_tied[centerX-ioff]) { X_AllTied = 0 ; centerX -= ioff ; break ; } if(centerX + ioff == fullXstop) break ; if(! x_tied[centerX+ioff]) { X_AllTied = 0 ; centerX += ioff ; break ; } } } centerY = (fullYstart + fullYstop) / 2 ; Y_AllTied = (m_tied_ranks.Size()) && (m_tied_ranks[centerY] != 0) ; if(Y_AllTied) { for(ioff=1 ; centerY-ioff >= fullYstart ; ioff++) { if(! m_tied_ranks[centerY-ioff]) { Y_AllTied = 0 ; centerY -= ioff ; break ; } if(centerY + ioff == fullYstop) break ; if(! m_tied_ranks[centerY+ioff]) { Y_AllTied = 0 ; centerY += ioff ; break ; } } } if(X_AllTied || Y_AllTied) splittable = 0 ; else { trialXstart[0] = trialXstart[1] = fullXstart ; trialXstop[0] = trialXstop[1] = centerX ; trialXstart[2] = trialXstart[3] = centerX+1 ; trialXstop[2] = trialXstop[3] = fullXstop ; trialYstart[0] = trialYstart[2] = fullYstart ; trialYstop[0] = trialYstop[2] = centerY ; trialYstart[1] = trialYstart[3] = centerY+1 ; trialYstop[1] = trialYstop[3] = fullYstop ; for(i=0 ; i<4 ; i++) expected[i] = (currentDataStop - currentDataStart + 1) * (trialXstop[i]-trialXstart[i]+1.0) / (fullXstop-fullXstart+1.0) * (trialYstop[i]-trialYstart[i]+1.0) / (fullYstop-fullYstart+1.0) ; actual[0] = actual[1] = actual[2] = actual[3] = 0 ; for(i=currentDataStart ; i<=currentDataStop ; i++) { k = m_indices[i] ; if(x[k] <= centerX) { if(m_ranks[k] <= centerY) ++actual[0] ; else ++actual[1] ; } else { if(m_ranks[k] <= centerY) ++actual[2] ; else ++actual[3] ; } } testval = 0.0 ; for(i=0 ; i<4 ; i++) { diff = fabs(actual[i] - expected[i]) - 0.5 ; testval += diff * diff / expected[i] ; } splittable = (testval > m_chi_crit) ? 1 : 0 ;
O método começa inicializando a pilha que controla o processo de divisão. Todo o conjunto de dados é inicialmente colocado na pilha como a partição raiz. O algoritmo, então, extrai iterativamente uma partição da pilha e avalia sua homogeneidade. Para verificar a significância estatística de possíveis dependências entre as variáveis dentro da partição, aplica-se o teste qui-quadrado. Se o teste indicar uma dependência significativa, a partição é dividida em quatro subdivisões, e cada subdivisão é colocada de volta na pilha para análise posterior. Esse processo continua até que todas as partições satisfaçam o critério de parada ou sejam consideradas homogêneas.
if(! splittable && fullXstop-fullXstart > 30 && fullYstop-fullYstart > 30) { ipx = fullXstart - 1 ; ipy = fullYstart - 1 ; for(i=0 ; i<4 ; i++) { xcut[i] = (fullXstop - fullXstart + 1) * (i+1) / 4 + fullXstart - 1 ; xfrac[i] = (xcut[i] - ipx) / (fullXstop - fullXstart + 1.0) ; ipx = xcut[i] ; ycut[i] = (fullYstop - fullYstart + 1) * (i+1) / 4 + fullYstart - 1 ; yfrac[i] = (ycut[i] - ipy) / (fullYstop - fullYstart + 1.0) ; ipy = ycut[i] ; } for(ix=0 ; ix<4 ; ix++) { for(iy=0 ; iy<4 ; iy++) { expected[ix*4+iy] = xfrac[ix] * yfrac[iy] * (currentDataStop - currentDataStart + 1) ; actual44[ix*4+iy] = 0 ; } } for(i=currentDataStart ; i<=currentDataStop ; i++) { k = m_indices[i] ; for(ix=0 ; ix<3 ; ix++) { if(x[k] <= xcut[ix]) break ; } for(iy=0 ; iy<3 ; iy++) { if(m_ranks[k] <= ycut[iy]) break ; } ++actual44[ix*4+iy] ; } testval = 0.0 ; for(ix=0 ; ix<4 ; ix++) { for(iy=0 ; iy<4 ; iy++) { diff = fabs(actual44[ix*4+iy] - expected[ix*4+iy]) - 0.5 ; testval += diff * diff / expected[ix*4+iy] ; } } splittable = (testval > 3 * m_chi_crit) ? 1 : 0 ; } } if(splittable) { for(i=currentDataStart ; i<=currentDataStop ; i++) current_indices[i] = m_indices[i] ; ipos = currentDataStart ; for(iSubRec=0 ; iSubRec<4 ; iSubRec++) { if(actual[iSubRec] >= 3) { stack[nstack].Xstart = trialXstart[iSubRec] ; stack[nstack].Xstop = trialXstop[iSubRec] ; stack[nstack].Ystart = trialYstart[iSubRec] ; stack[nstack].Ystop = trialYstop[iSubRec] ; stack[nstack].DataStart = ipos ; stack[nstack].DataStop = ipos + actual[iSubRec] - 1 ; ++nstack ; if(ArrayResize(stack,nstack+1,256)<0) { Print(__FUNCTION__," arrayresize error ", GetLastError()); return EMPTY_VALUE; } if(iSubRec == 0) { for(i=currentDataStart ; i<=currentDataStop ; i++) { k = current_indices[i] ; if(x[k] <= centerX && m_ranks[k] <= centerY) m_indices[ipos++] = current_indices[i] ; } } else if(iSubRec == 1) { for(i=currentDataStart ; i<=currentDataStop ; i++) { k = current_indices[i] ; if(x[k] <= centerX && m_ranks[k] > centerY) m_indices[ipos++] = current_indices[i] ; } } else if(iSubRec == 2) { for(i=currentDataStart ; i<=currentDataStop ; i++) { k = current_indices[i] ; if(x[k] > centerX && m_ranks[k] <= centerY) m_indices[ipos++] = current_indices[i] ; } } else { for(i=currentDataStart ; i<=currentDataStop ; i++) { k = current_indices[i] ; if(x[k] > centerX && m_ranks[k] > centerY) m_indices[ipos++] = current_indices[i] ; } } } else { if(actual[iSubRec] > 0) { px = (trialXstop[iSubRec] - trialXstart[iSubRec] + 1.0) / m_size ; py = (trialYstop[iSubRec] - trialYstart[iSubRec] + 1.0) / m_size ; pxy = (double) actual[iSubRec] / m_size ; MI += pxy * log(pxy / (px * py)) ; } } } } else { px = (fullXstop - fullXstart + 1.0) / m_size ; py = (fullYstop - fullYstart + 1.0) / m_size ; pxy = (currentDataStop - currentDataStart + 1.0) / m_size ; MI += pxy * log(pxy / (px * py)) ; } } return MI;
Para as partições consideradas homogêneas, seja por causa de um teste qui-quadrado não significativo ou por conterem um número mínimo de pontos de dados, a informação mútua é calculada. Esse cálculo inclui a estimativa das distribuições conjuntas e marginais de probabilidade dentro da partição e a aplicação da seguinte fórmula.
O processo continua de forma recursiva, em que as partições são divididas e avaliadas até que não seja mais possível realizar divisões significativas. A estimativa final da informação mútua é obtida somando as contribuições de todas as partições terminais, resultando em uma avaliação abrangente baseada na estrutura dos dados. Caso o método identifique um erro, ele retorna um valor equivalente à constante EMPTY_VALUE incorporada no MQL5. Na próxima seção, utilizaremos a classe Capm para implementar o algoritmo de seleção progressiva de características.
Seleção de preditores usando informação mútua
Ao aplicar a informação mútua à tarefa de seleção de características, nosso objetivo é identificar um subconjunto de preditores, a partir de um conjunto maior de candidatos, que maximize a dependência conjunta com a variável alvo. Essa dependência conjunta é uma generalização da informação mútua, em que uma das grandezas é um conjunto de variáveis em vez de uma única variável. O cálculo da dependência conjunta para grandes subconjuntos de preditores torna-se computacionalmente inviável devido à maldição da dimensionalidade. Isso ocorre porque a necessária partição multidimensional dos dados gera um número extremamente pequeno de células, especialmente à medida que o número de dimensões (preditores) aumenta.
Além disso, a enorme quantidade de combinações possíveis de características pode ser esmagadora mesmo em conjuntos de tamanho moderado. Esse explosão combinatória torna impraticável uma busca exaustiva por todos os subconjuntos possíveis, exigindo, portanto, o uso de estratégias de busca eficientes.
Uma abordagem comum para seleção de características é a seleção progressiva direta. Esse método começa com um modelo vazio e adiciona iterativamente a característica que proporciona a maior melhoria no desempenho do modelo, medida de acordo com o critério escolhido. Embora essa abordagem seja eficiente, ela sofre da limitação da "ganância". Isso significa que podem não ser encontradas combinações ótimas de características, como nos casos em que duas características juntas oferecem poder preditivo significativamente melhor do que qualquer uma delas isoladamente. Isso acontece porque o algoritmo se concentra em melhorias incrementais a cada etapa, podendo deixar de lado relações sinérgicas entre características.
Embora existam outros métodos, como os de ordem superior e o de seleção regressiva, a seleção progressiva direta continua sendo a escolha mais prática devido à sua eficiência computacional, especialmente quando se trabalha com grandes conjuntos de dados e recursos computacionais limitados. Além disso, quando aplicada ao contexto específico da dependência conjunta, a seleção progressiva direta apresenta uma propriedade notável: ela pode aproximar de forma eficaz o subconjunto ótimo de características, mesmo sem otimizar explicitamente a medida de dependência conjunta.
Peng, Long e Ding propuseram um algoritmo de seleção de características baseado em informação mútua, conhecido como critério de máxima relevância e mínima redundância (MRMR). Esse algoritmo aproxima de maneira eficiente o subconjunto ótimo de características sem a necessidade de calcular explicitamente a dependência conjunta, o que seria computacionalmente inviável. O critério MRMR é fundamentado nos conceitos de relevância e redundância. A relevância de um conjunto de características S em relação à variável alvo Y é definida como a média da informação mútua entre Y e cada característica em S.
Aqui, ∣S∣ representa o número de características em S, e I(Xi,Y) é a informação mútua entre a característica Xi e Y.
Embora a maximização da relevância seja intuitiva, ela pode resultar em um conjunto não ótimo devido à redundância. Se selecionarmos apenas com base na relevância individual de cada característica em relação à variável alvo, poderemos acabar com um conjunto de características altamente correlacionadas, que fornecem pouca informação adicional. Para superar esse problema, é necessário considerar não apenas a relevância de uma característica, mas também sua redundância em relação às características já selecionadas.
A redundância de um preditor potencial Xi em relação ao conjunto de características já selecionado S é definida como a média da informação mútua entre Xi e cada característica em S:
Onde I(Xi,Xj) representa a informação mútua entre o preditor potencial Xi e a característica Xj já presente em S.
É importante destacar que a formulação matemática para redundância é idêntica à de relevância. A única diferença está na interpretação. Quando calculamos a informação mútua entre um preditor potencial e a variável alvo, chamamos isso de relevância. Entretanto, quando a informação mútua é calculada entre um preditor potencial e uma característica já pertencente ao conjunto selecionado, chamamos isso de redundância.
Essencialmente, ambos os conceitos envolvem medir a quantidade de informação compartilhada entre duas variáveis, mas o contexto define se ela será considerada relevância ou redundância. No caso da relevância, observamos a relação entre a característica e a variável alvo, enquanto no caso da redundância o foco está no grau de sobreposição entre uma característica potencial e as já selecionadas.
Resumindo, o algoritmo MRMR funciona da seguinte forma:
- Como primeira característica, é selecionada aquela com a maior informação mútua em relação à variável alvo Y.
- Em cada etapa seguinte, o algoritmo escolhe a característica que maximiza o seguinte critério:
Esse critério equilibra a relevância da característica em relação à variável alvo Y (ou seja, I(X_i, Y)) com sua redundância em relação às características já selecionadas. Um aspecto notável do algoritmo MRMR é que ele consegue aproximar de forma eficiente o subconjunto ótimo de características, mesmo que a otimização direta da dependência conjunta seja computacionalmente inviável. Esse resultado, embora não seja imediatamente evidente, foi matematicamente demonstrado no artigo original.
Para avaliar a significância estatística das características selecionadas, podem ser aplicados dois testes de permutação de Monte Carlo (MCP):
- Significância individual da característica: Esse teste avalia a importância de cada característica individual ao comparar sua relevância em relação à variável alvo com uma distribuição nula obtida por meio de permutações. Ao embaralhar os valores das características, criamos uma distribuição nula que assume que a característica não tem relação com o alvo. Se a relevância observada for significativamente maior do que os valores permutados, podemos concluir que a característica provavelmente é de fato informativa.
- Significância global do modelo: Esse teste avalia a importância do conjunto completo de características selecionadas ao comparar a soma agregada da relevância individual com uma distribuição nula obtida por permutação. Ao embaralhar os valores da variável alvo, criamos uma distribuição nula que assume que as características não têm relação com o alvo. Se a relevância observada do conjunto for significativamente maior do que os valores permutados, podemos concluir que o subconjunto de características selecionadas provavelmente terá poder preditivo sobre a variável alvo.
Implementação da seleção progressiva em MQL5 baseada em informação mútua
A classe Cmrmr em mutualinfo.mqh implementa o algoritmo MRMR e em seu construtor recebe os seguintes parâmetros:- num_reps: Número de repetições para o teste de permutação de Monte Carlo. Definir esse parâmetro como um valor menor ou igual a 1 desativa os testes.
- max_preds: Número máximo de características a serem selecionadas.
- chisquare_thresh: Valor limite do teste qui-quadrado usado no método de partição adaptativa para estimar a informação mútua.
- m_verbose: Um sinalizador lógico que define se a saída detalhada deve ser exibida durante a execução do algoritmo.
//+-----------------------------------------------------------------------+ //|Relevance minus redundancy for building an optimal subset of predictors| //+-----------------------------------------------------------------------+ class Cmrmr { private: int m_max_preds; int m_reps; bool m_verbose; int m_samples; int m_vars; matrix m_preds; vector m_target; vector m_crits; double m_chisquare_thresh; ulong m_selected_vars[]; matrix m_critical_values; vector mutualinfo(int which_size,int &which[],vector &targs); public: Cmrmr(int num_reps,int max_preds, double chisquare_thresh,bool verbose); ~Cmrmr(void); bool StepWise(matrix &predictors, vector &targets); matrix GetCriticalValues(void); bool GetSelectedVars(ulong &output[]); };
Para iniciar o processo de seleção de características com a classe Cmrmr, é necessário instanciar um objeto dessa classe e, em seguida, chamar seu método StepWise(). O método StepWise() utiliza dois insumos principais:
- Matriz de variáveis preditoras potenciais: Essa matriz deve conter os dados de todas as características potenciais que o praticante deseja considerar.
- Vetor da variável alvo: Esse vetor deve conter os valores da variável alvo.
O método retorna um valor lógico indicando o sucesso ou falha do processo de seleção de características. O método StepWise() na classe Cmrmr inicializa as estruturas de dados necessárias e, em seguida, entra no ciclo de testes MCP. Se os testes MCP não forem solicitados (`num_reps` menor ou igual a 1), o ciclo é executado apenas uma vez. Dentro do ciclo, o método calcula a informação mútua entre cada preditor potencial e a variável alvo utilizando uma instância da classe Capm.
//+------------------------------------------------------------------+ //| Stepwise feature selection based on mutual information | //+------------------------------------------------------------------+ bool Cmrmr::StepWise(matrix &predictors,vector &targets) { if(m_selected_vars.Size()) ArrayFree(m_selected_vars); m_preds = predictors; m_samples = int(m_preds.Rows()); m_vars = int(m_preds.Cols()); m_max_preds = m_max_preds>=m_vars?m_vars:m_max_preds; m_target = targets; int i, j, k, ivar, irep; int index[], stepwise_mcpt_count[], solo_mcpt_count[], stepwise_ivar[], which_preds[],original_stepwise_ivar[] ; int nkept,best_ivar; vector casework(m_samples), sorted(m_samples), mutual(m_vars); vector crit(m_vars), relevance(m_vars), original_relevance(m_vars), current_crits(m_vars), sorted_crits(m_vars); double best_crit, dtemp, group_pvalue,solo_pvalue; vector stepwise_crit(m_vars), original_stepwise_crit(m_vars); double sum_relevance; vector original_sum_relevance(m_vars), sum_redundancy(m_vars); vector target = m_target; best_crit = -DBL_MAX; best_ivar = -1; nkept = m_max_preds; if(ArrayResize(index,m_vars)<0 || ArrayResize(stepwise_mcpt_count,m_vars)<0 || ArrayResize(solo_mcpt_count,m_vars)<0 || ArrayResize(which_preds,m_vars)<0 || ArrayResize(stepwise_ivar,m_vars)<0 || ArrayResize(original_stepwise_ivar,m_vars)<0) { Print(__FUNCTION__," array resize error ", GetLastError()); return false; } int unif_error; for(irep=0 ; irep<m_reps ; irep++) { if(irep) { i = m_samples ; while(i > 1) { j = (int)(MathRandomUniform(0.0,1.0,unif_error) * i); if(unif_error) { Print(__FUNCTION__," Mathrandomuniform error ", GetLastError()); return false; } if(j >= i) j = i - 1 ; dtemp = target[--i] ; target[i] = target[j] ; target[j] = dtemp ; } } for(i=0 ; i<m_vars ; i++) which_preds[i] = i ; crit = mutualinfo(m_vars,which_preds,target); for(ivar=0 ; ivar<m_vars ; ivar++) { relevance[ivar] = crit[ivar] ; if(ivar == 0 || crit[ivar] > best_crit) { best_crit = crit[ivar] ; best_ivar = ivar ; } } stepwise_crit[0] = best_crit ; stepwise_ivar[0] = best_ivar ; sum_relevance = best_crit ; if(irep == 0) { original_stepwise_crit[0] = best_crit ; original_stepwise_ivar[0] = best_ivar ; original_sum_relevance[0] = sum_relevance ; stepwise_mcpt_count[0] = 1 ; for(ivar=0 ; ivar<m_vars ; ivar++) { index[ivar] = ivar ; original_relevance[ivar] = sorted_crits[ivar] = current_crits[ivar] = crit[ivar] ; solo_mcpt_count[ivar] = 1 ; } if(!qsortdsi(0, m_vars-1, sorted_crits, index)) { Print(__FUNCTION__, " failed qsort "); return false; } if(m_verbose) Print(" Variable MI") ; for(i=m_vars-1 ; i>=0 ; i--) { k = index[i] ; if(m_verbose) PrintFormat("%15s %12.4lf",string(k), current_crits[k]) ; } } else { if(sum_relevance >= original_sum_relevance[0]) ++stepwise_mcpt_count[0] ; for(ivar=0 ; ivar<m_vars ; ivar++) { if(relevance[ivar] >= original_relevance[ivar]) ++solo_mcpt_count[ivar] ; } } for(i=0 ; i<m_vars ; i++) sum_redundancy[i] = 0.0 ; for(nkept=1 ; nkept<m_max_preds ; nkept++) { if(irep == 0 && m_verbose) { Print("Predictors so far Relevance Redundancy Criterion") ; for(i=0 ; i<nkept ; i++) { k = stepwise_ivar[i] ; PrintFormat("%15s %12.4lf %12.4lf %12.4lf",string(k), relevance[k], relevance[k] - stepwise_crit[i], stepwise_crit[i]) ; } } k = 0 ; for(i=0 ; i<m_vars ; i++) { for(j=0 ; j<nkept ; j++) { if(stepwise_ivar[j] == i) break ; } if(j == nkept) which_preds[k++] = i ; } if(k != (m_vars - nkept)) { Print(__FUNCTION__, " failed assertion ", __LINE__); return false; } k = stepwise_ivar[nkept-1] ; casework = m_preds.Col(k); crit = mutualinfo(m_vars-nkept,which_preds,casework); for(i=0 ; i<(m_vars-nkept) ; i++) { k = which_preds[i] ; sum_redundancy[k] += crit[i] ; index[i] = k ; sorted_crits[i] = current_crits[i] = ((relevance[k] - sum_redundancy[k]) / double(nkept)) ; if(i == 0 || current_crits[i] > best_crit) { best_crit = current_crits[i] ; best_ivar = k ; } } stepwise_crit[nkept] = best_crit ; stepwise_ivar[nkept] = best_ivar ; sum_relevance += relevance[best_ivar] ; if(irep == 0) { original_stepwise_crit[nkept] = best_crit ; original_stepwise_ivar[nkept] = best_ivar ; original_sum_relevance[nkept] = sum_relevance ; stepwise_mcpt_count[nkept] = 1 ; if(!qsortdsi(0, m_vars-nkept-1, sorted_crits, index)) { Print(__FUNCTION__, " failed qsort "); return false; } if(m_verbose) { Print("Additional candidates, in order of decreasing relevance minus redundancy") ; Print(" Variable Relevance Redundancy Criterion") ; for(i=m_vars-nkept-1 ; i>=0 ; i--) { k = index[i] ; PrintFormat("%15s %12.4lf %12.4lf %12.4lf", string(k), relevance[k], sum_redundancy[k] / nkept, relevance[k] - sum_redundancy[k] / nkept) ; } } } else { if(sum_relevance >= original_sum_relevance[nkept]) ++stepwise_mcpt_count[nkept] ; } } } //--- m_critical_values = matrix::Zeros(nkept,m_reps>1?5:3); //--- if(ArrayResize(m_selected_vars,nkept)<0) { Print(__FUNCTION__, " failed array resize ", GetLastError()); return false; } //--- if(m_verbose) { if(m_reps > 1) Print("Final predictors || Relevance || Redundancy || Criterion || Solo pval || Group pval") ; else Print("Final predictors || Relevance || Redundancy || Criterion") ; } //--- for(i=0 ; i<nkept ; i++) { k = original_stepwise_ivar[i] ; m_selected_vars[i] = ulong(k); m_critical_values[i][0] = original_relevance[k]; m_critical_values[i][1] = original_relevance[k] - original_stepwise_crit[i]; m_critical_values[i][2] = original_stepwise_crit[i]; if(m_critical_values.Cols()>3) { group_pvalue = (double) stepwise_mcpt_count[i] / (double) m_reps; solo_pvalue = (double) solo_mcpt_count[k] / (double) m_reps; m_critical_values[i][3] = solo_pvalue; m_critical_values[i][4] = group_pvalue; } if(m_verbose) { if(m_reps > 1) PrintFormat("%15s %12.4lf %12.4lf %12.4lf %8.3lf %8.3lf",string(k), m_critical_values[i][0], m_critical_values[i][1], m_critical_values[i][2],solo_pvalue,group_pvalue) ; else PrintFormat("%15s %12.4lf %12.4lf %12.4lf",string(k), m_critical_values[i][0], m_critical_values[i][1], m_critical_values[i][2]); } } return true; }
O método mutualinfo(), que é um método privado da classe Cmrmr, é responsável por estimar a informação mútua entre um determinado preditor e a variável alvo por meio do método de partição adaptativa. Ele retorna um vetor com as aproximações da informação mútua para os preditores selecionados, especificados como índices de colunas no mutualinfo().
//+------------------------------------------------------------------+ //| continuous mutual infomation | //+------------------------------------------------------------------+ vector Cmrmr::mutualinfo(int which_size,int &which[],vector &targs) { vector res = vector::Zeros(m_vars); res.Fill(-DBL_MAX); vector vars; Capm mia(true,targs,m_chisquare_thresh); for(int i = 0; i<which_size; i++) { vars = m_preds.Col(which[i]); res[i] = mia.fit(vars); } return res; }
O método StepWise() armazena os valores calculados de informação mútua em um vetor de "relevância", já que esses valores serão usados nas iterações seguintes para calcular o coeficiente de redundância. A característica com a maior informação mútua é escolhida como a primeira, e suas informações são armazenadas nas variáveis stepwise_crit e stepwise_ivar. Além disso, a variável sum_relevance é inicializada para acompanhar a relevância acumulada das características selecionadas, que mais tarde será utilizada nos testes MCP.
Na iteração inicial, sem permutações, o algoritmo guarda os valores originais de relevância e do critério para a primeira característica selecionada. Ele também inicializa os contadores para o teste global de significância do modelo e para os testes de significância individuais das características. Além disso, é exibida uma tabela mostrando a informação mútua de cada característica potencial em relação à variável alvo, a fim de fornecer uma visão do ranqueamento inicial das características. Essa tabela ajuda a visualizar o processo inicial de seleção e serve como referência para comparação com os valores permutados na etapa de testes de significância.
Se não estivermos na iteração inicial sem permutação, o algoritmo passa a executar os dois testes MCP, projetados para avaliar a significância estatística do conjunto de características selecionado pelo algoritmo MRMR. Para gerenciar de forma eficiente a crescente redundância à medida que novas características são adicionadas ao conjunto selecionado, o algoritmo utiliza o array `sum_redundancy`. Esse array é inicializado com zeros. Esse array é usado para armazenar a redundância acumulada de cada característica potencial em relação às já selecionadas.
No início, como nenhuma característica foi escolhida, todos os valores desse array permanecem zerados. Quando uma nova característica é adicionada ao conjunto selecionado, é necessário atualizar sua redundância em relação a cada uma das características potenciais restantes. A redundância de uma característica em relação ao conjunto selecionado em crescimento é definida como a média da informação mútua entre a característica potencial e as já escolhidas. Calcula-se a informação mútua entre a nova característica e cada uma das restantes, o que quantifica a quantidade de informação que a nova característica compartilha com cada uma delas. O valor de redundância de cada característica restante é atualizado somando-se a informação mútua entre a característica recém-adicionada e a já existente.
Após a atualização dos valores de redundância, o algoritmo calcula o critério MRMR (Máxima relevância, mínima redundância) para cada uma das características potenciais ainda não selecionadas. O critério MRMR é obtido subtraindo-se a redundância média de cada característica de sua relevância. Assim que o critério MRMR é calculado para todas as características restantes, é escolhida aquela com o maior valor de MRMR. O algoritmo repete esse processo de forma iterativa, atualizando o array `sum_redundancy` e recalculando o critério MRMR a cada nova adição. À medida que mais características são selecionadas, a redundância das restantes em relação ao conjunto escolhido é continuamente atualizada, garantindo que as relações dinâmicas entre características sejam consideradas durante todo o processo de seleção.
Se estivermos na replicação inicial, sem permutação, os valores originais dessas métricas são armazenados para comparação posterior durante os testes de permutação. Caso contrário, os contadores dos testes de permutação "globais" e "individuais" são incrementados. Após completar o número necessário de iterações, o algoritmo finaliza exibindo uma tabela resumida com as características selecionadas, sua relevância e os valores-p obtidos nos testes de permutação. Depois da chamada e execução bem-sucedida de `StepWise()`, é possível usar `GetCriticalValues()` para obter a matriz da tabela de valores exibida no modo detalhado. Já `GetSelectedVars()` extrai os índices das variáveis selecionadas (colunas), correspondentes à propriedade `m_max_preds` da classe.
//+------------------------------------------------------------------+ //| get the matrix of decision variables | //| matrix rows arranged in terms of descending relevance of each | //| candidate predictor. Most relevant predictors critical values are| //| in the first row, etc. | //| Column 0 is the calculated relevance | //| Column 1 is the calculated redundancy | //| Column 2 is the calculated Stepwise Mutual information | //| Column 3 and 4 will exist only if MCP test is specified | //| Column 3 is the Solo probability value | //| Column 4 are the Group probability values | //+------------------------------------------------------------------+ matrix Cmrmr::GetCriticalValues(void) { return m_critical_values; } //+--------------------------------------------------------------------------+ //|get the indices of the most relevant predictors as determined by algorithm| //+--------------------------------------------------------------------------+ bool Cmrmr::GetSelectedVars(ulong &output[]) { return (ArrayCopy(output,m_selected_vars)>0); }
In the next section, we will see how the Cmrmr class is applied using synthetic and real-world datasets.
Exemplos de relevância menos redundância
Para ilustrar de forma clara a aplicação do algoritmo MRMR, começamos aplicando-o a um conjunto de dados sintético. Esse conjunto é composto por 100 amostras e 10 variáveis preditoras potenciais (características). A variável alvo Y é calculada pela soma das quatro primeiras colunas da matriz, de modo que a relação entre a variável alvo e os preditores seja determinística nesses casos. No entanto, as colunas de 4 a 7 não contêm nenhuma informação real sobre a variável alvo e funcionam apenas como características irrelevantes. Já as variáveis nas colunas 8 e 9 representam combinações das variáveis com índices de colunas {1,3} e {0,2}, respectivamente.
//--- MathSrand(125); matrix rdata(100,10); rdata.Random(0.0,1.0); vector dep = rdata.Col(0) + rdata.Col(1) + rdata.Col(2) + rdata.Col(3); vector sum02 = rdata.Col(0) + rdata.Col(2); vector sum13 = rdata.Col(1) + rdata.Col(3); if(!rdata.Col(sum13,8) || !rdata.Col(sum02,9)) { Print(" Failed column insertion ", GetLastError()); return; }
O objetivo é usar o algoritmo MRMR para identificar quais colunas são os preditores mais relevantes. Esse experimento foi implementado no script RelevanceMinusRedundancy.mq5, que permite configurar diferentes hiperparâmetros, incluindo o número de permutações no método de Monte Carlo e o número máximo de características a serem selecionadas. O script foi configurado para ser executado em modo detalhado, fornecendo saídas completas durante o processo de seleção de características.
//inputs input int NumReplications = 100; input int MaxPredictors = 10; input double ChiSquareThreshold = 6.0;
Os dados de entrada do script RelevanceMinusRedundancy.mq5 são os valores individuais da informação mútua entre cada preditor potencial e a variável alvo. Esses valores de informação mútua são organizados em ordem decrescente, facilitando a identificação das características mais relevantes.
Variable MI 9 0.2675 8 0.1696 0 0.1662 3 0.1336 2 0.0823 1 0.0645 6 0.0000 7 0.0000 4 0.0000 5 0.0000
Como esperado, a primeira característica selecionada pelo algoritmo MRMR é aquela com a maior informação mútua individual em relação à variável alvo. Neste caso, trata-se da variável localizada na coluna de índice 9.
Predictors so far Relevance Redundancy Criterion 9 0.2675 0.0000 0.2675
A segunda característica selecionada é a variável da coluna de índice 8. Pelos resultados da análise, essa variável apresenta alta relevância, além de baixa redundância em relação à primeira característica selecionada.
Predictors so far Relevance Redundancy Criterion 9 0.2675 0.0000 0.2675 8 0.1696 0.0000 0.1696
A terceira característica selecionada mostra como a redundância influencia o processo de escolha. Embora as variáveis das colunas 0, 1, 2 e 3 estejam diretamente relacionadas à variável alvo, sua alta redundância em relação às já selecionadas (colunas 8 e 9) reduz o valor do critério MRMR. Assim, o algoritmo passa a selecionar características menos diretamente associadas, mas com menor redundância em relação ao conjunto existente. Somente após a inclusão de algumas variáveis não relacionadas é que o algoritmo começa a priorizar as características das colunas 0, 1, 2 e 3, uma vez que sua redundância em relação ao conjunto selecionado diminui.
Additional candidates, in order of decreasing relevance minus redundancy Variable Relevance Redundancy Criterion 5 0.0000 0.0000 0.0000 4 0.0000 0.0000 0.0000 7 0.0000 0.0000 0.0000 6 0.0000 0.0000 0.0000 1 0.0645 0.0600 0.0044 0 0.1662 0.1351 0.0311 2 0.0823 0.1426 -0.0603 3 0.1336 0.1781 -0.0445
Enquanto as variáveis irrelevantes neste exemplo sintético apresentaram relevância nula, é importante destacar que, em cenários reais, características irrelevantes ainda podem mostrar algum grau de correlação com a variável alvo, resultando em redundância diferente de zero. Nessas situações, o algoritmo MRMR equilibra de forma eficaz relevância e redundância, priorizando as características que oferecem informações únicas.
Final predictors || Relevance || Redundancy || Criterion || Solo pval|| Group pval 9 0.2675 0.0000 0.2675 0.010 0.010 8 0.1696 0.0000 0.1696 0.010 0.010 4 0.0000 0.0000 0.0000 1.000 0.010 5 0.0000 0.0000 0.0000 1.000 0.010 6 0.0000 0.0000 0.0000 1.000 0.010 7 0.0000 0.0000 0.0000 1.000 0.010 1 0.0645 0.0738 -0.0093 0.010 0.010 0 0.1662 0.1811 -0.0149 0.010 0.010 2 0.0823 0.1077 -0.0254 0.010 0.010 3 0.1336 0.1584 -0.0247 0.010 0.010
Os valores-p obtidos por meio dos testes de permutação de Monte Carlo fornecem informações valiosas sobre a significância estatística das características selecionadas. Valores-p mais baixos indicam evidências mais fortes de que a característica é de fato informativa e não fruto do acaso. Para determinar significância estatística, utiliza-se comumente o limiar de 0,05.
A seguir, é apresentada uma demonstração do algoritmo em um conjunto de dados mais realista. O script StepWiseFeatureSelectionByMutualInformation.mq5 mostra a aplicação prática do algoritmo MRMR em dados financeiros. Ele parte da premissa de prever a rentabilidade futura de um determinado ativo com base em um conjunto de indicadores técnicos. O script calcula quatro indicadores: Índice de Fluxo de Dinheiro (MFI), Média Móvel (MA), índice de força dos ursos e índice de força dos touros em diferentes janelas de retrospectiva. Esses indicadores funcionam como características potenciais no processo de seleção. Aplicando o algoritmo MRMR, o script busca identificar a combinação mais informativa de indicadores capazes de prever de forma eficaz a rentabilidade futura. Isso envolve selecionar os indicadores que melhor se relacionam com a variável alvo (rentabilidade futura), ao mesmo tempo em que se minimiza a redundância em relação às demais características escolhidas.
//+------------------------------------------------------------------+ //| StepWiseFeatureSelectionByMutualInformation.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs #resource "\\Indicators\\LogReturns.ex5" #include<mutualinfo.mqh> #include<ErrorDescription.mqh> //+------------------------------------------------------------------+ //|indicator type | //+------------------------------------------------------------------+ enum SELECT_INDICATOR { MFI=0,//MFI MA,//MA BEARS,//BEARS BULLS//BULLS }; //--- input parameters input int NumReplications = 100; input int MaxPredictors = 10; input double ChiSquareThreshold = 6.0; input bool VerboseOutPut = false; input uint period_inc=2;//lookback increment input uint max_lookback=6; input ENUM_MA_METHOD AppliedMA = MODE_SMA; input ENUM_APPLIED_PRICE AppliedPrice = PRICE_CLOSE; input datetime SampleStartDate=D'2019.12.31'; input datetime SampleStopDate=D'2022.12.31'; input string SetSymbol="BTCUSD"; input ENUM_TIMEFRAMES SetTF = PERIOD_D1; //---- string predictor_names[]; // variable names int size_sample, //training set size size_observations, //size of of both training and testing sets combined maxperiod, //maximum lookback indicator_handle=INVALID_HANDLE; //long moving average indicator handle //--- vector indicator[]; //indicator indicator values; //--- matrix feature_matrix; //full matrix of features; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //---get relative shift of sample set int samplestart,samplestop,num_features; samplestart=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStartDate); samplestop=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStopDate); num_features = int((max_lookback/period_inc)*4); //---check for errors from ibarshift calls if(samplestart<0 || samplestop<0) { Print(ErrorDescription(GetLastError())); return; } //---set the size of the sample sets size_observations=(samplestart - samplestop) + 1 ; maxperiod=int(max_lookback); //---check for input errors if(size_observations<=0 || maxperiod<=0) { Print("Invalid inputs "); return; } //---allocate memory if(ArrayResize(indicator,num_features+1)<0) { Print(ErrorDescription(GetLastError())); return; } //----get the full collection of indicator values int period_len; int k=0; //--- for(SELECT_INDICATOR select_indicator = 0; select_indicator<4; select_indicator++) { for(int iperiod=0; iperiod<int((indicator.Size()-1)/4); iperiod++) { period_len=int((iperiod+1) * period_inc); int try =10; predictor_names.Push(EnumToString(select_indicator)+"_"+string(period_len)); while(try) { switch(select_indicator) { case MFI: indicator_handle=iMFI(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len,VOLUME_TICK); break; case MA: indicator_handle=iMA(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len,0,AppliedMA,AppliedPrice); break; case BEARS: indicator_handle=iBearsPower(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len); break; case BULLS: indicator_handle=iBullsPower(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len); break; } if(indicator_handle==INVALID_HANDLE) try --; else break; } if(indicator_handle==INVALID_HANDLE) { Print("Invalid indicator handle ",EnumToString(select_indicator)," ", GetLastError()); return; } Comment("copying data to buffer for indicator ",period_len); try = 0; while(!indicator[k].CopyIndicatorBuffer(indicator_handle,0,samplestop,size_observations) && try <10) { try ++; Sleep(5000); } if(try <10) ++k; else { Print("error copying to indicator buffers ",GetLastError()); Comment(""); return; } if(indicator_handle!=INVALID_HANDLE && IndicatorRelease(indicator_handle)) indicator_handle=INVALID_HANDLE; } } //---resize matrix if(!feature_matrix.Resize(size_observations,indicator.Size()-1)) { Print(__LINE__); Print(ErrorDescription(GetLastError())); Comment(""); return; } //---copy collected data to matrix for(ulong i = 0; i<feature_matrix.Cols(); i++) if(!feature_matrix.Col(indicator[i],i)) { Print(__LINE__); Print(ErrorDescription(GetLastError())); Comment(""); return; } //--- int try = 10; while(try >-1 && indicator_handle == INVALID_HANDLE) { indicator_handle=iCustom(SetSymbol!=""?SetSymbol:NULL,SetTF,"\\Indicators\\LogReturns",0,1,1); try --; } //--- if(try <0) { Print("Could not initialize returns indicator "); Print(ErrorDescription(GetLastError())); Comment(""); return; } else { try = 10; } //--- while(try >-1 && !indicator[indicator.Size()-1].CopyIndicatorBuffer(indicator_handle,0,samplestop-1,size_observations)) { Sleep(2000); try --; } //--- if(try <0) { Print("Could not collect returns indicator info "); Print(ErrorDescription(GetLastError())); Comment(""); return; } else { IndicatorRelease(indicator_handle); Comment(""); } //--- display the dataset string console; for(uint i = 0; i<predictor_names.Size(); i++) console+=StringFormat(" %12s ",predictor_names[i]); console+=" NextBar Returns (Target) "; Print(console); for(ulong i = 0; i<feature_matrix.Rows(); i++) { console = ""; for(ulong j = 0; j<feature_matrix.Cols(); j++) console += StringFormat(" %12.6lf ",feature_matrix[i][j]); console+=StringFormat(" %12.6lf ",indicator[indicator.Size()-1][i]); Print(console); } //--- Cmrmr mstep(NumReplications,MaxPredictors,ChiSquareThreshold,VerboseOutPut); //--- if(!mstep.StepWise(feature_matrix,indicator[indicator.Size()-1])) return; //--- Print(" Final predictor labels "); ulong variables[]; if(mstep.GetSelectedVars(variables)) { for(uint i = 0; i<variables.Size(); i++) Print(predictor_names[variables[i]]); } return; } //+------------------------------------------------------------------+Abaixo está um trecho do conjunto de dados de indicadores coletados no terminal MetaTrader 5, juntamente com a variável alvo na última coluna. No total, existem 12 preditores potenciais (indicadores).
MFI_2 MFI_4 MFI_6 MA_2 MA_4 MA_6 BEARS_2 BEARS_4 BEARS_6 BULLS_2 BULLS_4 BULLS_6 NextBar Returns(Target) 0.000000 53.151442 46.921608 7213.654000 7279.552750 7260.007333 -76.371267 -101.867797 -107.113146 87.265733 61.769203 56.523854 -0.003564 0.000000 26.316595 34.451328 7180.962000 7244.558000 7255.420333 -41.795089 -70.889078 -83.462247 42.344911 13.250922 0.677753 -0.032447 0.000000 0.000000 36.720977 7053.740000 7133.697000 7204.281833 -127.731696 -210.813447 -251.244462 153.948304 70.866553 30.435538 0.054562A seguir estão os resultados da análise dos preditores usando os parâmetros padrão do script. Primeiro, observamos a tabela de valores de informação mútua. Aqui, vemos que alguns indicadores apresentam informação mútua nula em relação ao alvo. O indicador de média móvel com período igual a 6 possui a maior informação mútua em relação ao alvo.
PS Variable MI ND 5 0.0308 LG 4 0.0293 MJ 3 0.0279 GM 6 0.0227 JP 9 0.0182 IS 1 0.0165 MF 8 0.0135 JI 7 0.0126 HL 0 0.0000 JO 2 0.0000 GS 10 0.0000 HD 11 0.0000
Naturalmente, o MA_6 é a primeira escolha feita pelo algoritmo. Como essa é a primeira seleção, torna-se interessante verificar se existem outros preditores potenciais que compartilham informações com ele (MA_6). Isso pode ser feito analisando os valores de redundância mostrados a seguir.
ME Predictors so far Relevance Redundancy Criterion GH 5 0.0308 0.0000 0.0308 II Additional candidates, in order of decreasing relevance minus redundancy FE Variable Relevance Redundancy Criterion LE 0 0.0000 0.0095 -0.0095 ED 1 0.0165 0.0869 -0.0704 FD 2 0.0000 0.1149 -0.1149 PE 11 0.0000 0.2768 -0.2768 ID 8 0.0135 0.3251 -0.3116 NS 7 0.0126 0.3492 -0.3366 CR 10 0.0000 0.3484 -0.3484 ES 6 0.0227 0.4030 -0.3802 IS 9 0.0182 0.4285 -0.4103 CR 3 0.0279 2.5363 -2.5084 DR 4 0.0293 3.0096 -2.9803A próxima seleção do algoritmo é o MFI_2, já que ele apresenta a menor redundância entre todos os preditores potenciais restantes, apesar de sua relevância nula em relação à variável alvo. Também vale destacar que os preditores nas colunas de índice 3 e 4 (MA_2 e MA_4, respectivamente) possuem os maiores níveis de redundância com o MA_6. Isso é esperado, pois trata-se do mesmo indicador calculado com janelas menores. Abaixo está o preditor escolhido após mais duas seleções, ambas também com relevância nula em relação à variável alvo. É importante observar o preditor principal no conjunto de candidatos que não foi selecionado. O algoritmo passa a considerar os preditores relevantes em virtude do efeito de diluição causado pelas duas últimas seleções no conjunto escolhido como um todo.
PQ Predictors so far Relevance Redundancy Criterion FL 5 0.0308 0.0000 0.0308 JK 0 0.0000 0.0095 -0.0095 DK 2 0.0000 0.1796 -0.1796 EE Additional candidates, in order of decreasing relevance minus redundancy JH Variable Relevance Redundancy Criterion CH 6 0.0227 0.1469 -0.1241 PH 9 0.0182 0.1465 -0.1283 GI 10 0.0000 0.2024 -0.2024 PG 7 0.0126 0.2133 -0.2007 PF 11 0.0000 0.2308 -0.2308 GG 8 0.0135 0.2664 -0.2529 QG 1 0.0165 0.4679 -0.4514 KF 3 0.0279 0.8840 -0.8561 LF 4 0.0293 1.0372 -1.0079Encerramos nosso comentário sobre os resultados avançando até o conjunto final de preditores selecionados pelo algoritmo.
OQ Final predictors || Relevance || Redundancy || Criterion || Solo pval || Group pval HN 5 0.0308 0.0000 0.0308 0.010 0.010 DJ 0 0.0000 0.0095 -0.0095 1.000 0.010 JG 2 0.0000 0.1796 -0.1796 1.000 0.010 HD 6 0.0227 0.1620 -0.1393 0.010 0.010 FQ 9 0.0182 0.2023 -0.1842 0.010 0.010 DL 11 0.0000 0.2798 -0.2798 1.000 0.010 KJ 1 0.0165 0.2932 -0.2767 0.010 0.010 OG 7 0.0126 0.3298 -0.3172 0.010 0.010 OE 10 0.0000 0.4151 -0.4151 1.000 0.010 JP 8 0.0135 0.4585 -0.4450 0.010 0.010 RN Final predictor labels NO MA_6 DE MFI_2 NN MFI_6 KP BEARS_2 DJ BULLS_2 FL BULLS_6 PQ MFI_4 MK BEARS_4 FM BULLS_4 OF BEARS_6
Considerações finais
Neste artigo, foi analisada a aplicação da informação mútua na seleção de características. O foco principal foi o algoritmo MRMR (máxima dependência, máxima relevância, mínima redundância), proposto por Peng, Long e Ding. Iniciamos com a discussão sobre a avaliação da informação mútua para variáveis contínuas, ressaltando sua capacidade de capturar dependências não lineares complexas entre características e a variável alvo. Ao longo da investigação do algoritmo MRMR, mostramos como ele equilibra de maneira eficaz as tarefas concorrentes de maximizar a relevância em relação à variável alvo e, ao mesmo tempo, minimizar a redundância entre as características já selecionadas.
Por meio da adição iterativa de características com base em seus valores MRMR e da avaliação de sua significância estatística usando os testes de permutação de Monte Carlo, o algoritmo garante que o conjunto selecionado forneça os preditores mais informativos e únicos para o modelo. Os exemplos sintéticos e reais demonstraram a utilidade prática do algoritmo MRMR em cenários reais de processamento de dados, nos quais características irrelevantes e multicolinearidade frequentemente dificultam o processo de seleção. A capacidade do algoritmo MRMR de priorizar características relevantes e evitar redundância garante que ele continue sendo uma ferramenta valiosa na seleção de características, especialmente em situações em que as relações entre variáveis são complexas e não lineares.
Todo o código citado neste artigo está incluído abaixo. A tabela a seguir descreve todos os arquivos de código-fonte que acompanham o artigo.
Nome do arquivo | Descrição |
---|---|
MQL5/include/np.mqh | Arquivo de cabeçalho com funções utilitárias de matrizes e vetores. |
MQL5/include/mutualinfo.mqh | Arquivo de cabeçalho que contém a definição da classe Capm, que implementa o método de partição adaptativa para estimar a informação mútua de variáveis contínuas. E a definição da classe Cmrmr, que implementa a seleção progressiva de características baseada em informação mútua. |
MQL5/scripts/RelevanceMinusRedundancy.mq5 | Script que demonstra o uso da classe Cmrmr com dados sintéticos. |
MQL5/scripts/StepWiseFeatureSelectionByMutualInformation.mq5 | Outro script que demonstra a aplicação da classe Cmrmr em um conjunto de dados mais realista. |
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/16416
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso