Outubro de 2017

Volume 32 - Número 10

Execução de Teste - Regressão da Série Temporal Usando uma Rede Neutra C#

Por James McCaffrey

James McCaffreyA meta de um problema de regressão da série temporal é fazer previsões com base em dados temporais históricos. Por exemplo, se você tem dados de vendas mensais (durante um ano ou dois), talvez você queira prever as vendas para o próximo mês. Uma regressão de série temporal é geralmente muito difícil e há muitas técnicas que você pode usar.

Neste artigo, demonstrarei como realizar uma análise de regressão de série temporal usando dados de janela rolante combinados a uma rede neural. A ideia é melhor explicada por um exemplo. Veja o programa de demonstração na Figura 1. O programa de demonstração analisa o número de passageiros de linha aérea que viajaram a cada mês entre janeiro de 1949 e dezembro de 1960.

Demonstração de regressão de série temporal de janela rolante

Figura 1 Demonstração de regressão de série temporal de janela rolante

Os dados de demonstração vêm de um conjunto de dados de benchmark que você pode encontrar em muitos locais na Internet e é incluído com o download que acompanha este artigo. Os dados brutos têm esta aparência:

"1949-01";112
"1949-02";118
"1949-03";132
"1949-04";129
"1949-05";121
"1949-06";135
"1949-07";148
"1949-08";148
...
"1960-11";390
"1960-12";432

Há 144 itens de dados brutos. O primeiro campo é o ano e o mês. O segundo campo é o número total de passageiros de linha aérea internacional para o mês, em milhares. A demonstração cria dados de treinamento usando uma janela rolante de tamanho 4, resultando em 140 itens de treinamento. Os dados de treinamento são normalizados dividindo-se cada contagem de passageiros por 100:

[  0]   1.12   1.18   1.32   1.29   1.21
[  1]   1.18   1.32   1.29   1.21   1.35
[  2]   1.32   1.29   1.21   1.35   1.48
[  3]   1.29   1.21   1.35   1.48   1.48
...
[139]   6.06   5.08   4.61   3.90   4.32

Observe que os valores temporais explícitos nos dados são removidos. A primeira janela consiste das primeiras quatro contagens de passageiros (1.12, 1.18, 1.32 e 1.29), que são usadas como valores preditores, seguidas pela quinta contagem (1.21) que é um valor a prever. A próxima janela consiste da segunda à quinta contagens (1.18, 1.32, 1.29 e 1.21), que são o próximo conjunto de valores preditores, seguidas pela sexta contagem (1.35) que é o valor a prever. Para resumir, cada conjunto de quatro contagens de passageiros consecutivas é usado para prever a próxima contagem.

A demonstração cria uma rede neural com quatro nós de entrada, 12 nós de processamento ocultos e um único nó de saída. O número de nós de entrada corresponde ao número de preditores na janela rolante. O tamanho da janela deve ser determinado por tentativa e erro, que é o principal ponto negativo dessa técnica. O número de nós ocultos da rede neural também deve ser determinado por tentativa e erro, o que é sempre verdadeiro para redes neurais. Há apenas um nó de saída, porque a regressão de série temporal prevê uma unidade de tempo à frente.

A rede neural tem (4 * 12) + (12 * 1) = 60 node-to-node pesos de nó para nó e (12 + 1) = 13 desvios, o que essencialmente define o modelo da rede neural. Ao usar os dados da janela rolante, o programa de demonstração treina a rede usando o algoritmo de retropropagação estocástico básico com uma taxa de aprendizado definida como 0,01 e um número fixo de iterações definido como 10.000.

Durante o treinamento, a demonstração mostra o erro de valor quadrático entre os valores de saída previstos e os valores de saída corretos, a cada 2.000 iterações. O erro de treinamento é difícil de interpretar e é monitorado principalmente para ver se algo realmente estranho acontece (o que é bastante comum). Nesse caso, o erro parece se estabilizar após cerca de 4.000 iterações.

Após o treinamento, o código de demonstração exibe os 73 pesos e valores de desvio, de novo principalmente como uma verificação de integridade. Para problemas de regressão de série temporal, você deve usar tipicamente uma métrica de precisão personalizada. Aqui, uma previsão correta é aquela em que a contagem de passageiros prevista não normalizada tem uma diferença de até 30 para mais ou menos em relação à contagem real. Com essa definição, o programa de demonstração atingiu 91,43% de precisão, o que significa 128 previsões corretas e 12 erradas para as 140 contagens de passageiros previstas.

A demonstração conclui usando a rede neural treinada para prever a contagem de passageiros para janeiro de 1961, o primeiro período de tempo após o intervalo dos dados de treinamento. Isso se chama extrapolação. A previsão é de 433 passageiros. Esse valor pode ser usado como uma variável preditora para prever o valor para fevereiro de 1961 e assim por diante.

Este artigo pressupõe que você tem habilidades de programação intermediárias ou acima disso e que tem conhecimento básico sobre redes neurais, mas não pressupõe que você saiba nada sobre regressão de séries temporais. Codifiquei o programa de demonstração usando a linguagem C#, mas você não deve ter muita dificuldade para refatorar o código em outra linguagem, como Java ou Python. O programa de demonstração é um pouco longo para ser apresentado por completo, mas o código-fonte está disponível no download de arquivo que acompanha este artigo.

Regressão de série temporal

Problemas de regressão de série temporal muitas vezes são exibidos usando um gráfico de linhas como aquele na Figura 2. A linha azul indica as 144 contagens de passageiros reais e não normalizadas, em milhares, de janeiro de 1949 a dezembro de 1960. A linha de cor vermelho-claro indica as contagens de passageiros previstas geradas pelo modelo de série temporal da rede neural. Observe que, já que o modelo usa uma janela rolante com quatro valores preditores, a primeira contagem de passageiros prevista não ocorre até que mês = 5. Adicionalmente, fiz previsões para nove meses além do intervalo dos dados de treinamento. Elas são indicadas pela linha vermelha pontilhada.

Gráfico de linhas de regressão de série temporal

Figura 2 Gráfico de linhas de regressão de série temporal

Além de fazer previsões para tempos além do intervalo de dados de treinamento, as análises de regressão de série temporal podem ser usadas para identificar pontos de dados anômalos. Isso não ocorre com os dados de contagem de passageiros de demonstração – você pode ver que as contagens previstas correspondem com bastante proximidade às contagens reais. Por exemplo, a contagem de passageiros real para o mês t = 67 é 302 (o ponto azul próximo ao centro na Figura 2) e a contagem prevista é 272. Mas suponha que a contagem real para o mês t = 67 fosse 400. Haveria uma indicação visual óbvia de que a contagem real para o mês 67 era um valor de exceção.

Você também pode usar uma abordagem programática para identificar dados anômalos com regressão de série temporal. Por exemplo, você pode sinalizar qualquer valor temporal em que o valor de dados real e o valor previsto diferiram por mais do que um limite fixado, tal como quatro vezes o desvio padrão dos valores dos dados previstos versus os valores dos dados reais.

O programa de demonstração

Para codificar o programa de demonstração, iniciei o Visual Studio e criei um novo aplicativo do console C# e o nomeei como Neural-TimeSeries. Usei o Visual Studio 2015, mas o programa de demonstração não tem dependências significativas do .NET Framework, portanto, qualquer versão relativamente recente funcionará bem.

Após o código do modelo ser carregado na janela do editor, cliquei com o botão direito do mouse no arquivo Program.cs na janela do Gerenciador de Soluções e renomeei o arquivo como NeuralTimeSeriesProgram.cs, depois permiti que o Visual Studio renomeasse automaticamente a classe Program para mim. Na parte superior do código gerado por modelo, excluí tudo o que era desnecessário usando instruções, exceto o que faz referência ao namespace do Sistema de nível superior.

A estrutura geral do programa, com algumas edições secundárias para economizar espaço, é apresentada na Figura 3.

Figura 3 Estrutura do programa NeuralTimeSeries

using System;
namespace NeuralTimeSeries
{
  class NeuralTimeSeriesProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin times series demo");
      Console.WriteLine("Predict airline passengers ");
      Console.WriteLine("January 1949 to December 1960 ");

      double[][] trainData = GetAirlineData();
      trainData = Normalize(trainData);
      Console.WriteLine("Normalized training data:");
      ShowMatrix(trainData, 5, 2, true);  // first 5 rows

      int numInput = 4; // Number predictors
      int numHidden = 12;
      int numOutput = 1; // Regression

      Console.WriteLine("Creating a " + numInput + "-" + numHidden +
        "-" + numOutput + " neural network");
      NeuralNetwork nn = new NeuralNetwork(numInput, numHidden,
        numOutput);

      int maxEpochs = 10000;
      double learnRate = 0.01;
      double[] weights = nn.Train(trainData, maxEpochs, learnRate);
      
      Console.WriteLine("Model weights and biases: ");
      ShowVector(weights, 2, 10, true);

      double trainAcc = nn.Accuracy(trainData, 0.30); 
      Console.WriteLine("\nModel accuracy (+/- 30) on training " +
        "data = " + trainAcc.ToString("F4"));

      double[] future = new double[] { 5.08, 4.61, 3.90, 4.32 };
      double[] predicted = nn.ComputeOutputs(future); 
      Console.WriteLine("January 1961 (t=145): ");
      Console.WriteLine((predicted[0] * 100).ToString("F0"));

      Console.WriteLine("End time series demo ");
      Console.ReadLine();
    } // Main

    static double[][] Normalize(double[][] data) { . . }

    static double[][] GetAirlineData() {. . }

    static void ShowMatrix(double[][] matrix, int numRows,
      int decimals, bool indices) { . . }

    static void ShowVector(double[] vector, int decimals,
      int lineLen, bool newLine) { . . }

  public class NeuralNetwork { . . }
} // ns

A demonstração usa uma rede neural de uma única camada oculta simples, implementada do zero. Como alternativa, você pode usar as técnicas apresentadas neste artigo juntamente com uma biblioteca de rede neural como o Kit de Ferramentas Cognitivas da Microsoft.

A demonstração começa configurando os dados de treinamento, conforme mostrado na Figura 4.

Figura 4 Configurando os dados de treinamento

double[][] trainData = GetAirlineData();
trainData = Normalize(trainData);
Console.WriteLine("Normalized training data:");
ShowMatrix(trainData, 5, 2, true);

Method GetAirlineData is defined as:

static double[][] GetAirlineData()
{
  double[][] airData = new double[140][];
  airData[0] = new double[] { 112, 118, 132, 129, 121 };
  airData[1] = new double[] { 118, 132, 129, 121, 135 };
...
  airData[139] = new double[] { 606, 508, 461, 390, 432 };
  return airData;
}

Aqui, os dados de janela rolante são codificados com um tamanho de janela igual a 4. Antes de escrever o programa de série temporal, escrevi um pequeno programa utilitário para gerar os dados de janela rolante com base nos dados brutos. Na maioria dos cenários que não são de demonstração, você leria dados brutos de um arquivo de texto e, em seguida, geraria programaticamente dados de janela rolante em que o tamanho da janela é parametrizado, de modo que você poderia fazer experiências com tamanhos diferentes.

O método Normalize apenas divide todos os valores de dados por um 100 constante. Eu fiz isso por motivos exclusivamente práticos. Minhas primeiras tentativas usando dados não normalizados levaram a resultados muito ruins, mas após a normalização, meus resultados foram bem melhores. Em teoria, ao trabalhar com redes neurais, seus dados não precisam ser normalizados, mas na prática, a normalização muitas vezes faz uma grande diferença.

A rede neural é criada assim:

int numInput = 4; 
int numHidden = 12;
int numOutput = 1; 
NeuralNetwork nn =
  new NeuralNetwork(numInput, numHidden, numOutput);

O número de nós de entrada é definido como quatro, porque cada janela rolante tem quatro valores preditores. O número de nós de saída é definido como um, porque cada conjunto de valores de janela é usado para fazer uma previsão para o mês seguinte. O número de nós ocultos é definido como 12 e foi determinado por tentativa e erro.

A rede neural é treinada e avaliada com as seguintes instruções:

int maxEpochs = 10000;
double learnRate = 0.01;
double[] weights = nn.Train(trainData, maxEpochs, learnRate);
ShowVector(weights, 2, 10, true);

O método Train usa retropropagação básica. Há muitas variações, incluindo o uso de impulso ou de taxas de aprendizado adaptativas para aumentar a velocidade de treinamento, bem como usar o desligamento ou a regularização de L1 ou L2 para evitar o sobreajuste do modelo. O método auxiliar ShowVector exibe um vetor com valores reais formatados para duas casas decimais com dez valores por linha.

Após o modelo de série temporal de rede neural ter sido criado, a precisão de previsão dele é avaliada:

double trainAcc = nn.Accuracy(trainData, 0.30);
Console.WriteLine("\nModel accuracy (+/- 30) on " + 
  " training data = " + trainAcc.ToString("F4"));

Para regressão de série temporal, decidir se um valor previsto está correto ou não depende do problema sendo investigado. Para os dados de passageiros de linha aérea, o método Accuracy marca uma contagem de passageiros prevista como correta se a contagem prevista não normalizada tem uma diferença de até 30 para mais ou menos em relação à contagem real. Para os dados de demonstração, as primeiras cinco previsões para t = 5 a t = 9 são corretas, mas a previsão para t = 10 é incorreta:

t  actual  predicted
= = = = = = = = = = =
 5   121     129
 6   135     128
 7   148     137
 8   148     153
 9   136     140
10   119     141

O programa de demonstração termina usando as quatro últimas contagens de passageiros (t = 141 a 144) para prever a contagem de passageiros para o primeiro período de tempo além do intervalo dos dados de treinamento (t = 145 = janeiro de 1961):

double[] predictors = new double[] { 5.08, 4.61, 3.90, 4.32 };
double[] forecast = nn.ComputeOutputs(predictors); 
Console.WriteLine("Predicted for January 1961 (t=145): ");
Console.WriteLine((forecast[0] * 100).ToString("F0"));
Console.WriteLine("End time series demo");

Observe que, devido ao modelo de série temporal ter sido treinado usando dados normalizados (divididos por 100), as previsões também serão normalizadas, então a demonstração exibirá os valores previstos vezes 100.

Redes neurais para análises de série temporal

Quando você define uma rede neural, você deve especificar as funções de ativação usadas pelos nós de camada oculta e pelos nós de camada de saída. Brevemente, recomendo o uso da função de tangente hiperbólica (tanh) para ativação oculta e a função de identidade para ativação de saída.

Ao usar um sistema ou biblioteca de rede neural como o Kit de Ferramentas Cognitivas da Microsoft ou o Azure Machine Learning, você deve especificar explicitamente as funções de ativação. O programa de demonstração codifica essas funções de ativação. O código mais importante ocorre no método ComputeOutputs. Os valores de nó oculto são calculados assim:

for (int j = 0; j < numHidden; ++j) 
  for (int i = 0; i < numInput; ++i)
    hSums[j] += this.iNodes[i] * this.ihWeights[i][j];

for (int i = 0; i < numHidden; ++i)  // Add biases
  hSums[i] += this.hBiases[i];

for (int i = 0; i < numHidden; ++i)   // Apply activation
  this.hNodes[i] = HyperTan(hSums[i]); // Hardcoded

Aqui, a função HyperTan é definida no programa para evitar valores extremos:

private static double HyperTan(double x) {
  if (x < -20.0) return -1.0; // Correct to 30 decimals
  else if (x > 20.0) return 1.0;
  else return Math.Tanh(x);
}

Uma alternativa comum e razoável ao uso de tanh para ativação de nó oculto é usar a função sigmóide logística estritamente relacionada. Por exemplo:

private static double LogSig(double x) {
  if (x < -20.0) return 0.0; // Close approximation
  else if (x > 20.0) return 1.0;
  else return 1.0 / (1.0 + Math.Exp(x));
}

Já que a função de identidade é apenas f(x) = x, usá-la para ativação de nó de saída é apenas um modo sofisticado de dizer para não usar nenhuma ativação explícita. O código de demonstração no método ComputeOutputs é:

for (int j = 0; j < numOutput; ++j) 
  for (int i = 0; i < numHidden; ++i)
    oSums[j] += hNodes[i] * hoWeights[i][j];

for (int i = 0; i < numOutput; ++i)  // Add biases
  oSums[i] += oBiases[i];

Array.Copy(oSums, this.oNodes, oSums.Length);

A soma dos produtos para um nó de saída é copiado diretamente no nó de saída sem a aplicação de uma ativação explícita. Observe que o membro oNodes da classe NeuralNetwork é uma matriz com uma célula, em vez de uma única variável.

A escola das funções de ativação afeta o código no algoritmo de retropropagação implementado no método Train. O método Train usa os derivativos de cálculo de cada função de ativação. O derivativo de y = tanh(x) é (1 + y) * (1 - y). No código de demonstração:

// Hidden node signals
for (int j = 0; j < numHidden; ++j) {
  derivative = (1 + hNodes[j]) * (1 - hNodes[j]); // tanh
  double sum = 0.0; 
  for (int k = 0; k < numOutput; ++k)
    sum += oSignals[k] * hoWeights[j][k]; 
  hSignals[j] = derivative * sum;
}

Se você usar a ativação sigmóide logística, o derivativo de y = logsig(x) será y * (1 - y). Para a ativação de saída, o derivativo de cálculo de y = x é apenas a constante 1. O código relevante no método Train é:

for (int k = 0; k < numOutput; ++k) {
  errorSignal = tValues[k] - oNodes[k];
  derivative = 1.0;  // For Identity activation
  oSignals[k] = errorSignal * derivative;
}

Obviamente, multiplicar por 1 não tem nenhum efeito. Eu codifiquei desse modo para atuar como uma forma de documentação.

Conclusão

Há muitas técnicas que você pode usar para realizar análises de regressão de série temporal. O artigo da Wikipedia sobre o assunto lista dúzias de técnicas, classificadas de muitas maneiras, tais como paramétrico vs não paramétrico e linear vs não linear. Em minha opinião, a principal vantagem de usar uma abordagem de rede neural usando dados de janela rolante é que o modelo resultante muitas vezes (mas não sempre) é mais preciso do que o obtido com modelos não neurais. A principal desvantagem da abordagem com rede neural é que você precisa experimentar com a taxa de aprendizado para obter bons resultados.

A maioria das técnicas de análise de regressão de série temporal usam dados de janela rolante ou um esquema similar. No entanto, há técnicas avançadas que podem usar dados brutos, sem uso de janelas. Em específico, uma abordagem relativamente nova usa o que é chamado de uma rede neural com memória de curto prazo. Essa abordagem muitas vezes produz modelos preditivos muito precisos.


Dr. James McCaffreytrabalha para a Microsoft Research em Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo Internet Explorer e Bing. Entre em contato com o Dr. McCaffrey pelo email jamccaff@microsoft.com.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: John Krumm, Chris Lee e Adith Swaminathan


Discuta esse artigo no fórum do MSDN Magazine