Março de 2016

Volume 31 Número 3

Test Run - Regressão de rede neural

Por James McCaffrey

James McCaffreyO objetivo de um problema de regressão é prever o valor de uma variável numérica (normalmente chamada de variável dependente) com base nos valores de uma ou mais variáveis de previsão (as variáveis independentes), que podem ser numéricas ou categóricas. Por exemplo, talvez você queira prever a renda anual de uma pessoa com base em idade, em sexo (masculino ou feminino) e em anos de escolaridade.

A forma mais simples de regressão é chamada de regressão linear (LR). Uma equação de previsão pode ter a aparência a seguir: renda = 17,53 + (5,11 * idade) + (-2,02 * homem) + (-1,32 * mulher) + (6.09 * escolaridade). Apesar de a LR ser útil para alguns problemas, em muitas situações ela não é eficaz. Mas há outros tipos comuns de regressão: a regressão polinomial, a regressão de modelo linear geral e a regressão de rede neural (NNR). Esse último tipo de regressão é indiscutivelmente a forma mais poderosa de regressão.

O tipo mais comum de rede neural (NN) é aquele que prevê uma variável categórica. Por exemplo, talvez você queira prever a inclinação política de uma pessoa (conservadora, moderada, liberal) com base em fatores como idade, renda e sexo. Um classificador de NN possui n nós de saída, onde n é o número de valores que a variável dependente pode assumir. A soma dos valores dos nós de saída n é 1,0 e eles podem ser vagamente interpretados como probabilidades. Dessa forma, para prever a inclinação política, um classificador NN teria três nós de saída. Se os valores de nó de saída forem (0,24, 0,61, 0,15), o classificador estará prevendo uma inclinação “moderada”, já que o nó central possui a maior probabilidade.

Na regressão NN, a NN possui um único nó de saída que contém o valor previsto da variável numérica dependente. Então, para o exemplo que prevê a renda anual, haveria três nós de entrada (um para idade, um para sexo, onde masculino = -1 e feminino = +1, e um para anos de escolaridade) e um nó de saída (renda anual).

Uma boa maneira de entender o que é a regressão NN e de ver aonde esse artigo quer chegar é examinar o programa de demonstração na Figura 1. Em vez de lidar com um problema realista, para manter as ideias de regressão NN as mais claras possíveis, o objetivo da demonstração é criar um modelo de NN que possa prever o valor da função seno. Caso você esteja um pouco enferrujado nos seus conhecimentos de trigonometria, o gráfico da função seno é mostrado na Figura 2. A função seno aceita um valor de entrada real único do infinito negativo até o infinito positivo e retorna um valor entre -1,0 e +1,0. A função seno retorna 0 quando x = 0,0, x = pi (~3,14), x = 2 * pi, x= 3 * pi e assim por diante. A função seno é uma função surpreendentemente difícil de modelar.

Demonstração da regressão de rede neural
Figura 1 Demonstração da regressão de rede neural

A função Sen(x)
Figura 2 A função Sen(x)

A demonstração começa com a geração programática de 80 itens de dados a serem usados para treinamento do modelo de NN. Os 80 itens de treinamento têm um valor de entrada x aleatório entre 0 e 6,4 (um pouco acima de 2 * pi) e um valor correspondente y, que é o sen(x).

A demonstração cria uma NN 1-12-1, ou seja, uma NN com um nó de saída (para x), 12 nós de processamento ocultos (que efetivamente definem a equação de previsão) e um nó de saída (o seno de x previsto). Ao trabalhar com NNs, sempre há experimentação envolvida; o número de nós ocultos foi determinado por ensaio e erro.

Os classificadores de NN possuem duas funções de ativação, uma para nós ocultos e uma para nós de saída. A função de ativação do nó de saída para um classificador é quase sempre a função softmax, já que essa função produz valores cuja soma é 1,0. A função de ativação do nó oculto para um classificador normalmente é a função sigmoide ou a função tangente hiperbólica (abreviada como tanh). Entretanto, na regressão de NN, existe uma função de ativação de nó oculto, mas nenhuma função de ativação do nó de saída. A demonstração de NN usa a função tanh para a ativação do nó oculto.

A saída de uma NN é determinada por seus valores de entrada e por um conjunto de constante chamado de pesos e desvios. Como os desvios são na verdade somente tipos especiais de pesos, o termo “pesos”, algumas vezes, refere-se a ambos. Uma rede neural com i nós de entrada, j nós ocultos e k nós de saída possui um total de (i * j) + j + (j * k) + k pesos e desvios. Portanto, a NN de demonstração 1-12-1 possui (1 * 12) + 12 + (12 * 1) + 1 = 37 pesos e desvios.

O processo de determinação dos valores dos pesos e desvios é chamado de treinamento do modelo. A ideia é experimentar diferentes valores de pesos e desvios para determinar em que ponto os valores de saída calculados da NN correspondem de perto aos valores de saída correto conhecidos dos dados do treinamento.

Existem vários algoritmos diferentes que podem ser usados para treinar uma NN. A abordagem mais comum é usar o algoritmo de propagação de retorno. A propagação de retorno é um processo iterativo no qual os valores dos pesos e desvios mudam lentamente, de forma que a NN normalmente calcula valores de saída mais precisos.

A propagação de retorno usa dois parâmetros obrigatórios (número máximo de iterações e taxa de aprendizagem) e um parâmetro opcional (a taxa do momento). O parâmetro maxEpochs define um limite para o número de iterações do algoritmo. O parâmetro learnRate controla o quanto os valores dos pesos e desvios podem ser alterados em cada iteração. O parâmetro momentum acelera o treinamento e também ajuda a impedir que o algoritmo de propagação de retorno fique preso em um solução insatisfatória. A demonstração define o valor de maxEpochs como 10.000, o valor de learnRate como 0,005 e o valor de momentum como 0,001. Esses valores foram determinados por ensaio e erro.

Quando usamos o algoritmo de propagação de retorno para treinamento de NN, há três variações que podem ser usadas. Na propagação de retorno em lote, todos os itens do treinamento são examinados primeiro e, em seguida, todos os valores dos pesos e desvios são ajustados. Na propagação de retorno aleatória (também chamada de propagação de retorno online), após cada item do treinamento ser examinado, todos os valores dos pesos e desvios são ajustados. Na propagação de retorno em mini-lotes, todos os valores dos pesos e desvios são ajustados após o exame de uma fração especificada dos itens do treinamento. O programa de demonstração usa a variante mais comum, a propagação de retorno aleatória.

O programa de demonstração exibe uma medida de erro a cada 1.000 épocas de treinamento. Observe que os valores de erro variam um pouco. Após a conclusão do treinamento, a demonstração exibe os valores dos 37 pesos e desvios que definem o modelo da NN. Os valores dos pesos e desvios da NN não possuem uma interpretação óbvia, mas é importante examinar os valores para verificar os resultados equivocados como, por exemplo, quando um peso possui um valor muito grande e todos os outros pesos são próximos a zero.

O programa de demonstração é concluído com a avaliação do modelo de NN. Os valores previstos da NN de sen(x) para x = pi, pi/2 e 3 * pi/2 estão todos dentro de 0,02 dos valores corretos. O valor previsto para sen(6 * pi) está muito longe do valor correto. Mas esse é um resultado esperado, pois a NN foi treinada somente para prever os valores de sen(x) para x valores entre 0 e 2 * pi.

Este artigo pressupõe que você tenha habilidades de programação que sejam pelo menos intermediárias, mesmo que não saiba muito sobre a regressão de rede neural. O programa de demonstração é codificado usando a linguagem C#, mas você não deve ter muita dificuldade para refatorar o código em outra linguagem, como Visual Basic ou Perl. O programa de demonstração é um pouco longo para ser apresentado por completo neste artigo, mas o código-fonte está disponível no download do código que acompanha este artigo. Todas as verificações de erros normais foi removida da demonstração para manter as ideias principais tão claras quanto possível e o tamanho do código pequeno.

Estrutura do programa de demonstração

Para criar o programa de demonstração, eu iniciei o Visual Studio e selecionei o modelo do aplicativo do console do C# na ação de menu Arquivo | Novo | Projeto. Usei o Visual Studio 2015, mas a demonstração programa não tem dependências significativas do .NET e, portanto, funcionará em qualquer versão do Visual Studio. Nomeei o projeto como NeuralRegression.

Depois de carregar o código do modelo na janela Editor, na janela Gerenciador de Soluções selecionei o arquivo Program.cs, cliquei com o botão direito do mouse nele para renomeá-lo com o nome mais descritivo NeuralRegressionProgram.cs. Permiti que o Visual Studio renomeasse automaticamente a classe Program para mim. Na parte superior do código do Editor, excluí todas as referências de namespaces não usados, deixando somente a referência ao namespace do System de nível superior.

A estrutura geral do programa de demonstração, com algumas edições secundárias para economizar espaço, é mostrada na Figura 3. Todas as instruções de controle estão no método Main. Toda a funcionalidade da regressão de rede neural está contida em um classe definida pelo programa, NeuralNetwork.

Figura 3 Demonstração da regressão de rede neural

using System;
namespace NeuralRegression
{
  class NeuralRegressionProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin NN regression demo");
      Console.WriteLine("Goal is to predict sin(x)");
      // Create training data
      // Create neural network
      // Train neural network
      // Evaluate neural network
      Console.WriteLine("End demo");
      Console.ReadLine();
    }
    public static void ShowVector(double[] vector,
      int decimals, int lineLen, bool newLine) { . . }
    public static void ShowMatrix(double[][] matrix,
      int numRows, int decimals, bool indices) { . . }
  }
  public class NeuralNetwork
  {
    private int numInput; // Number input nodes
    private int numHidden;
    private int numOutput;
    private double[] inputs; // Input nodes
    private double[] hiddens;
    private double[] outputs;
    private double[][] ihWeights; // Input-hidden
    private double[] hBiases;
    private double[][] hoWeights; // Hidden-output
    private double[] oBiases;
    private Random rnd;
    public NeuralNetwork(int numInput, int numHidden,
      int numOutput, int seed) { . . }
    // Misc. private helper methods
    public void SetWeights(double[] weights) { . . }
    public double[] GetWeights() { . . }
    public double[] ComputeOutputs(double[] xValues) { . . }
    public double[] Train(double[][] trainData,
      int maxEpochs, double learnRate,
      double momentum) { . . }
  } // class NeuralNetwork
} // ns

No método Main, os dados de treinamento são criados pelas seguintes instruções:

int numItems = 80;
double[][] trainData = new double[numItems][];
Random rnd = new Random(1);
for (int i = 0; i < numItems; ++i) {
  double x = 6.4 * rnd.NextDouble();
  double sx = Math.Sin(x);
  trainData[i] = new double[] { x, sx };
}

Como regra geral ao lidar com redes neurais, quanto mais dados de treinamento, melhor. Para modelar a função seno para x valores entre 0 e 2 * pi, eu precisei de pelo menos 80 itens para obter bons resultados. A escolha de um valor de propagação de 1 para o objeto de número aleatório foi arbitrária. Os dados de treinamento são armazenados em uma matriz do tipo matriz de matrizes. Em cenários reais, você provavelmente leria os dados de treinamento de um arquivo texto.

A rede neural é criada com as seguintes instruções:

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

Existe somente um nó de entrada pois a função seno de destino aceita somente um único valor. Para a maioria dos problemas de regressão de rede neural, você terá vários nós de entrada, um para cada uma das variáveis independentes da previsão. Na maioria dos problemas de regressão de rede neural, existe somente um único nó de saída, mas é possível prever dois ou mais valores numéricos.

Uma NN precisa de um objeto aleatório para inicializar os valores de peso e para misturar a ordem na qual os itens do treinamento são processados. O construtor da NeuralNetwork de demonstração aceita um valor de propagação para o objeto aleatório interno. O valor usado, 0, foi arbitrário.

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

int maxEpochs = 10000;
double learnRate = 0.005;
double momentum  = 0.001;
double[] weights = nn.Train(trainData, maxEpochs,
  learnRate, momentum);
ShowVector(weights, 4, 8, true);

Uma NN é extremamente sensível aos valores de parâmetro do treinamento. Mesmo uma pequena alteração pode gerar um resultado totalmente diferente.

O programa de demonstração avalia a qualidade do modelo de NN resultante prevendo o sen(x) para os três valores padrão. As instruções, com algumas pequenas edições, são:

double[] y = nn.ComputeOutputs(new double[] { Math.PI });
Console.WriteLine("Predicted =  " + y[0]);
y = nn.ComputeOutputs(new double[] { Math.PI / 2 });
Console.WriteLine("Predicted =  " + y[0]);
y = nn.ComputeOutputs(new double[] { 3 * Math.PI / 2.0 });
Console.WriteLine("Predicted = " + y[0]);

Observe que a NN de demonstração armazena as suas saídas em uma matriz de nós de saída, mesmo com esse exemplo tendo somente um único valor de saída. Retornar uma matriz permite prever diversos valores sem alterar o código-fonte.

A demonstração é concluída com a previsão do sen(x) para um valor x que está totalmente fora do intervalo dos dados do treinamento:

y = nn.ComputeOutputs(new double[] { 6 * Math.PI });
Console.WriteLine("Predicted =  " + y[0]);
Console.WriteLine("End demo");

Na maioria dos cenários com o classificador de NN, você chama um método que calcula a precisão da classificação, ou seja, o número de previsões corretas dividido pelo número total de previsões. Isso é possível porque um valor de saída categórico é correto ou incorreto. Mas ao trabalhar com regressão de NN, não há uma maneira padrão de definir a precisão. Caso você queira calcular uma medida de precisão, ela dependerá do problema. Por exemplo, para prever o sen(x), você pode definir arbitrariamente uma previsão correta que esteja em 0,01 do valor correto.

Cálculo dos valores de saída

A maioria das principais diferenças entre uma NN projetada para classificação e uma projetada para regressão ocorre nos métodos que calculam a saída e treinam o modelo. A definição da classe NeuralNetwork do método ComputeOutputs começa com:

public double[] ComputeOutputs(double[] xValues)
{
  double[] hSums = new double[numHidden];
  double[] oSums = new double[numOutput];
...

O método aceita uma matriz que contém os valores das variável independentes da previsão. As variáveis locais hSums e oSums são matrizes de rascunho que contêm valores preliminares (antes da ativação) de nós ocultos e de saída. A seguir, os valores da variável independente são copiados para os nós de entrada da rede neural:

for (int i = 0; i < numInput; ++i)
  this.inputs[i] = xValues[i];

Depois os valores preliminares dos nós ocultos são calculados multiplicando-se cada valor de entrada pelo seu peso de entrada em relação a oculto correspondente e acumulam-se os mesmos:

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

Em seguida, os valores de desvio do nó oculto são adicionados:

for (int j = 0; j < numHidden; ++j)
  hSums[j] += this.hBiases[j];

Os valores dos nós ocultos são determinados aplicando-se a função de ativação do nó oculto a cada soma preliminar:

for (int j = 0; j < numHidden; ++j)
  this.hiddens[j] = HyperTan(hSums[j]);

A seguir, os valores preliminares dos nós de saída são calculados multiplicando-se cada valor do nó oculto pelo seu peso oculto para saída correspondente e acumulando-os:

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

Então, os valores de desvio do nó oculto são adicionados:

for (int k = 0; k < numOutput; ++k)
  oSums[k] += oBiases[k];

Até agora, calcular os valores do nó oculto para uma rede de regressão é exatamente a mesma coisa que calcular os valores do nó oculto para uma rede do classificador. Mas, em um classificador, os valores do nó de saída final seriam calculados aplicando-se a função de ativação softmax a cada soma acumulada. Para uma rede de regressão, nenhuma função de ativação é aplicada. Portanto, o método ComputeOutputs é finalizado simplesmente copiando os valores da matriz de rascunho de oSums diretamente para os nós de saída:

...
  Array.Copy(oSums, this.outputs, outputs.Length);
  double[] retResult = new double[numOutput]; // Could define a GetOutputs
  Array.Copy(this.outputs, retResult, retResult.Length);
  return retResult;
}

Por conveniência, os valores dos nós de saída também são copiados para uma matriz de retorno local para que possam ser facilmente acessados sem chamar algum tipo de método GetOutputs.

Ao treinar um classificador de NN usando o algoritmo de propagação de retorno, as derivadas das duas funções de ativação são utilizadas. Para os nós ocultos o código seria algo como:

for (int j = 0; j < numHidden; ++j) {
  double sum = 0.0; // sums of output signals
  for (int k = 0; k < numOutput; ++k)
    sum += oSignals[k] * hoWeights[j][k];
  double derivative = (1 + hiddens[j]) * (1 - hiddens[j]);
  hSignals[j] = sum * derivative;
}

O valor para a derivada nomeada da variável local é a derivada da função tanh e é fruto de uma teoria bastante complexa. Em um classificador de NN, o cálculo que envolve a derivada da função de ativação do nó de saída é:

for (int k = 0; k < numOutput; ++k) {
  double derivative = (1 - outputs[k]) * outputs[k];
  oSignals[k] = (tValues[k] - outputs[k]) * derivative;
}

Aqui, o valor para a derivada da variável local é a derivada da função softmax. Entretanto, como a regressão de NN não usa uma função de ativação para os nós de saída, o código é:

for (int k = 0; k < numOutput; ++k) {
  double derivative = 1.0;
  oSignals[k] = (tValues[k] - outputs[k]) * derivative;
}

Naturalmente, multiplicar por 1,0 não surte nenhum efeito, então você pode simplesmente abandonar o termo da derivada. Outra maneira de pensar é que na regressão de NN, a função de ativação do nó de saída é a função identidade f(x) = x. A derivada da função identidade é a constante 1,0.

Conclusão

O código de demonstração e a explicação neste artigo devem ser suficientes para você começar a explorar a regressão de rede neural com uma ou mais variáveis de previsão numérica. Caso você possua uma variável de previsão que seja categórica, precisará codificar a variável. Para uma variável de previsão categórica que pode assumir um de dois valores possíveis como, por exemplo, sexo (masculino, feminino), você codificaria um valor como -1 e outro como +1.

Para uma variável de previsão categórica que pode assumir um de três valores possíveis, você usaria a chamada codificação 1-de-(N-1). Por exemplo, se uma variável categórica for uma cor que pode assumir um de quatro valores possíveis (vermelho, azul, verde, amarelo), então vermelho seria codificado como (1, 0, 0), azul como (0, 1, 0), verde como (0, 0, 1) e amarelo como (-1, -1, -1).


Dr. James McCaffrey* trabalha 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 jammc@microsoft.com.*

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Gaz Iqbal e Umesh Madan