Redes Neurais

Esse artigo é uma tradução de: http://www.shiffman.net/teaching/nature/nn/ escrito por Daniel Shiffman.

Exemplos

Código de exemplo: neural_network.zip

Biblioteca de rede neural (necessária para 2 dos exemplos): nn.zip

perceptron exemplo

perceptron simples

xor exemplo

rede neural multi-camada

number exemplo

reconhecimento de padrões pixel

Lendo

  • The Computational Beauty of Nature, Gary William Flake, Chapter 22 — Neural Networks and Learning

Um Perceptron Simples

Começamos nossa discussão sobre Redes Neurais com um Perceptron simples. Um Perceptron é um modelo computacional de um neurônio. É constituído por uma ou mais entradas, um processamento e uma única saída. Cada entrada recebe um peso e o processamento cria a saída via os seguintes passos:

  • Para cada entrada, é multiplicado a entrada pelo seu peso;
  • Soma de todas as entradas vezes seus pesos, soma ponderada;
  • Computação da saída do Perceptron é baseada na soma passada para uma função de ativação.

A função de ativação simples é o sinal da soma. Em outras palavras, se a soma for um número positivo, a saída é 1, se é negativo, a saída é -1. Aqui está um código que assume um array de entradas, e um array de pesos de entradas.

// Soma todos os valores
float sum = 0;
int result = 0;
for (int i = 0; i < inputs.length; i++) {
  sum += inputs[i]*weights[i];
}
if (sum > 0) result = 1;
else result = -1;

perceptron classificacao

Um Perceptron é normalmente utilizado para classificação. Por exemplo, digamos que um Perceptron tem 2 entradas. Usando uma função de sinal de ativação, a saída será -1 ou 1, ou seja, os dados de entrada são classificados de acordo com o sinal de saída. Ele pertence ao grupo A (-1) ou B(1).

Considere uma linha no espaço de 2 dimensões. Os pontos nesse espaço podem ser classificados levando em consideração um lado e outro da linha de separação. Embora este seja um exemplo bastante trivial (já que podemos determinar o lado em que situa-se um ponto, com alguma álgebra simples), um Perceptron podem ser treinados para aprender a reconhecer os pontos de um lado contra o outro.

Nesse exemplo , há duas entradas, a coordenada x do ponto e coordenada y. No entanto, para que o Perceptron funcione corretamente, é preciso também incorporar um “bias” de entrada. Um “bias” sempre tem o valor de 1. Porque um “bias” é necessário? Sem “bias”, se todas as entradas forem 0, a única saída possível sempre será um zero (porque a soma de um monte de zeros é só zero!). Portanto, a estrutura do nosso Perceptron é a seguinte:

perceptron bias

Com base na estrutura acima, podemos criar a classe para descrever o Perceptron. O Perceptron inclui um array de pesos de entrada, bem como uma constante de aprendizado (a constante de aprendizado será utilizada no algoritmo de treinamento descrito abaixo).

class Perceptron {
  float[] weights; // Array dos pesos de entrada
  float c; // Constante de aprendizado

O construtor irá criar o array de pesos de entrada com base no número de entradas e define a constante de aprendizado.

  // Perceptron é criado com n pesos e constante de aprendizado
  Perceptron(int n, float c_) {
    weights = new float[n];
    // Inicia com pesos randômicos
    for (int i = 0; i < weights.length; i++) {
      weights[i] = random(-1, 1);
    }
    c = c_;
  }

O algoritmo de adivinhação é exatamente o que nós desenvolvemos acima, a soma de todas as entradas multiplicados pelos seus pesos. No exemplo abaixo, as entradas que chegam com o argumento “vals” (algum array de pontos fluentes) para a função. O último elemento do array é portanto o “bias” e deve ter o valor 1.

  // Adivinhar -1 ou 1 com base nos valores de entrada
  int guess(float[] vals) {
    // Soma de todos os valores
    float sum = 0;
    for (int i = 0; i < weights.length; i++) {
      sum += vals[i]*weights[i];
    }
    // Resultado é um sinal de soma, -1 ou 1
    int result = 1;
    if (sum < 0) result = -1;
    return result;
  }

A ponto de fazer tudo isso, claro, é ser capaz de treinar a rede para ser capaz de fazer a escolha certa. Isto é conseguido através da alimentação das entradas da rede com saídas corretas “conhecidas”, e ajustando os pesos de acordo com os erros de saída. O erro é calculado como segue:

// erro = saída desejada - saída adivinhada
ERROR = DESIRED OUTPUT - GUESS OUTPUT

Nesse exemplo simples, o erro somente pode ser um dos três valores: 0 (se desejado e adivinhado são iguais), 2 (se desejado = 1 e adivinhado = -1) e -2 (se desejado = -1 e adivinhado = 1). A seguinte fórmula é usada para determinar o quanto mudar os pesos. Note que, se o erro é igual a zero, os pesos não serão alterados desde que deltaweight seja igual a zero. Isso é como as coisas deveriam funcionar, pois se o erro é igual a zero, temos a resposta certa — não corrigi-lo se ele não está quebrado!

DELTAWEIGHT = LEARNINGCONSTANT * ERROR * INPUT
NEWWEIGHT = OLDWEIGHT + DELTAWEIGHT

No código, isso se traduz em uma função de aprendizado:

  // Função para aprendizado do Perceptron
  // Pesos são ajustados com base na resposta de "desejada"
  void train(float[] vals, int desired) {
    // Resultado de adivinhação
    int result = guess(vals);
    // Calcula o fator de mudança do peso com base no erro
    // erro = saída esperada - saída adivinhada
    // Observe que este só pode ser 0, -2, ou 2
    // Multiplicar pela constante de aprendizado
    float weightChange = c*(desired - result);
    // Ajusta os pesos com base no weightChange * input
    for (int i = 0; i < weights.length; i++) {
      weights[i] += weightChange * vals[i];
    }
  }

Para ter uma noção de como funciona a constante de aprendizado, baixe o exemplo de Perceptron e altere o valor no construtor:

ptron = new Perceptron(3, 0.004);

A maior constante fará com que o Perceptron aprenda mais rápido, mas pode ultrapassar o peso ideal. A baixa constante fará com que ele aprenda mais lentamente, mas ele provavelmente vai encontrar a melhor solução com mais precisão.

Agora que a classe Perceptron está construída, podemos treiná-lo para classificar os pontos do espaço, como descrito acima. Vamos escolher uma fórmula arbitrária de uma linha:

y = 0.9x - 0.2

Nós podemos fazer isso em um função:

// Função que descreve a linha
float f(float x) {
  return x*0.9-0.2;
}

E usá-lo para calcular um resultado conhecido. As entradas são x, y e 1 (bias) e o resultado conhecido é a variável de entrada.

float x = random(-2, 2);
float y = random(-2, 2);
int answer = 1;
if (y < f(x)) answer = -1;

Multi-Camadas de Rede Neural

O Perceptron é bom para resolver problemas simples que têm uma solução linearmente separáveis. Em outras palavras, todos os pontos de saída 1 vão cair de um lado de uma linha no espaço 2D e todos os pontos de saída -1 vão cair do outro lado. Perceptrons, no entanto, não podem resolver problemas linearmente separáveis.

O exemplo mais clássico de um problema linearmente inseparáveis é o XOR. XOR é “exclusivo”, ou seja, se A ou B, mas não ambos A e B.

AND
----
FALSE FALSE --> FALSE
TRUE  FALSE --> FALSE
FALSE TRUE  --> FALSE
TRUE  TRUE  --> TRUE
 
OR
----
FALSE FALSE --> FALSE
TRUE  FALSE --> TRUE
FALSE TRUE  --> TRUE
TRUE  TRUE  --> TRUE
 
XOR
---
FALSE FALSE --> FALSE
TRUE  FALSE --> TRUE
FALSE TRUE  --> TRUE
TRUE  TRUE  --> FALSE

perceptron linha xorConside os pontos (0,0), (1,0), (0,1) e (1,1) no espaço. Para XOR, não há como estabelecer uma única linha que separa os pontos verdadeiros: (1,0) e (0,1) a partir dos pontos falsos: (0,0) e (1,1). XOR é linearmente inseparáveis. Portanto, um Perceptron não pode ser treinado para desenvolver os pesos que irá produzir a saída correta (verdadeiro ou falso), dando duas entradas.

A fim de resolver XOR, nós temos que criar um Perceptron multi-camada com duas entradas, uma camada “escondida”, e uma saída. Esta camada oculta eleva-se para ter vários perceptrons para atacar o mesmo problema, ou seja, se “OU” é verdadeiro (perceptron #1) e “E” não é verdadeiro (perceptron #2), então a saída é XOR.

perceptron multi camada

A questão agora torna-se o treinamento da rede multi-camadas para ter os pesos certos. Infelizmente, isso envolve um pouco de cálculo. Essa página vai oferecer apenas um breve panaroma da solução. Para uma explicação mais detalhada, leia o capítulo 22 em The Computational Beauty of Nature. Você pode também simplesmente google multi-layered neural network backpropogation .

Back Propogation

Uma solução para treinamento de uma rede neural multi-camada é conhecido como backpropogation. Certamente, existem outras opções para treinamento de uma rede neural. Por exemplo, um algoritmo genético pode ser usado para pesquisar pesos ótimos!

Para gerar a saída da rede, as entradas multiplicadas pelos pesos são somados e alimentado para a frente através da rede. Isto é o que nós fazemos com o perceptron, só há uma camada adicional entre a entrada e a saída. O treinamento da rede envolve assumir o erro (desejado – adivinhado) e alimentá-lo para trás através da rede. Assim que o erro final informa ambos os pesos entre a camada escondida e a saída, bem como os pesos entre as entradas e a camada escondida.

Vamos primeiro olhar como nós alimentamos para a frente através da rede. Nossa rede neural será composta das seguintes classes:

  • Neuron – cada objeto neurônio consiste de uma lista de objetos Connection (ou seja, indicando o que outros Neurons estão conectados). Ele tem a função que calcula a sua saída com base em suas entradas multiplicado pelos pesos da conexão relevante. Ele também sabe se é um Neuron “bias” ou apenas um Neuron regular. O exemplo também tem subclasses InputNeuron, HiddenNeuron e OutputNeuron para qualquer funcionalidade específica para os neurônios;
  • Connection – cada objeto Connection contém dois Neurons, o “de” Neuron e o “para” Neuron. Tem também uma variável do tipo double para o seu peso;
  • Network – o objeto Network consiste de todos os Neurons. Um array de neurônios de entrada, neurônios escondidos, e um neurônio saída (embora isso possa, e deve, concebivelmente ser um array). Ele também terá funções que alimentam a rede para frente e treinam a rede para trás.

sigmoidAssim, o primeiro passo para a alimentação para frente da rede é calcular a saída de todos os neurônios na camada escondida. Em um perceptron simples, a saída era o sinal da soma das entradas * pesos de conexão. Se essa soma foi positiva, a saída era +1, se fosse negativa, a saída era -1. O sinal da soma era nossa função de ativação.

Para que backpropogation funcione bem, vamos precisar de função de ativação fancier. Usaremos uma função sigmóide. Aqui está um applet com os gráficos da função sigmóide . Olhando para o gráfico, podemos ver que a função de ativação sigmóide nos diz muito mais sobre a nossa saída do que a função do sinal de ativação faz. Quando o neurônio dispara para perto do ponto 1/2 (meio do gráfico), a inclinação da curva da sigmóide é muito íngreme. Quando ele é acionado perto das bordas do exterior (os lados esquerdo ou direito da imagem à direita), a inclinação é de nível. O código da sigmóide é o seguinte:

// Função sigmóide
float f(float x) {
  return 1.0 / (1.0 + exp(-x));
}

dsigmoidA inclinação da curva da sigmóide é o valor que queremos saber, uma vez que nos diz onde estamos na ativação continuum – perto do centro ou perto da borda? A inclinação é calculada pela derivada (a taxa de variação de uma função) da sigmóide. Este deve ser um conceito familiar para nós – a taxa de variação (isto é, derivada) da localização é a velocidade, da velocidade é a aceleração . A derivada da função sigmóide é f(x) * (1-f(x)) e esse valor será usado no algoritmo de backpropogation para alterar os pesos.

Ok, de volta à alimentação para a frente! Agora que conhecemos que a função de ativação é a função sigmóide, podemos calcular a saída para qualquer neurônio dada como a soma de todos os valores de entrada * os pesos de conexão passados através da sigmóide. Note que nossos neurônios podem ter conexões entrando e saindo, mas para obter nossa saída, nós queremos apenas as conexões que entram! Além disso, se o neurônio é um neurônio bias, o cálculo não será necessário, pois a saída é sempre 1.

for (int i = 0; i < connections.size(); i++) {
  Connection c = (Connection) connections.get(i);
  Neuron from = c.getFrom();
  Neuron to = c.getTo();
  // É neste contexto que se deslocam para a frente
  if (to == this) {
      sum += from.getOutput()*c.getWeight();
    }
  }
}
output = f(sum);

Agora que os neurônios sabem como calcular suas saídas, nós podemos pegar os valores de entrada e alimentar para frente. Primeiro, nós alimentamos nas entradas, então fazemos loop em todos os neurônios escondidos e calculamos suas saídas, e então nós calculamos a saída do do neurônio de saída em si! Aqui é um trecho de exemplo (na Network) classe:

public double feedForward(double[] inputVals) {
  // Primeiro colocamos os valores de entrada na rede
  //System.out.println("\n\nFeeding input layer");
  for (int i = 0; i < inputVals.length; i++) {
    input[i].input(inputVals[i]);
  }
  // Então calculamos a saída para a camada escondida
  //System.out.println("\n\nFeeding hidden layer");
  for (int i = 0; i < hidden.length-1; i++) {
    //System.out.println("\nNeuron " + i);
    hidden[i].calcOutput();
  }
 
  //System.out.println("\n\nFeeding output layer");
  output.calcOutput();
  return output.getOutput();
}

Uma vez que temos a saída da rede neural, podemos comparar a saída para uma resposta conhecida (a partir do XOR) e ajustar os pesos de acordo.

Com o perceptron simples, foi calculado o erro como:

// erro = desejado - adivinhado
ERROR = DESIRED - GUESS

Vamos agora usar o fato que a a derivada da função sigmóide nós contou mais informações sobre o erro e o cálculo da seguinte maneira:

// erro = adivinhado*(1-adivinhado) * (desejado-adivinhado)
ERROR = GUESS*(1-GUESS) * (DESIRED-GUESS)

Esse erro é aplicado aos pesos entre a saída e a camada escondida.

DELTAWEIGHT = LEARNINGCONSTANT * ERROR * HIDDEN OUTPUT
NEWWEIGHT = OLDWEIGHT + DELTAWEIGHT

O próximo passo é calcular o erro para a saída do neurônio escondido. Essa é a parte mais difícil, uma vez que nós não temos uma resposta conhecida! Sabemos apenas qual o resultado final que toda rede deve ser! A fórmula para o erro da camada escondida é, portanto, uma função de derivação da sua própria saída e que o erro global de toda a rede.

HIDDENERROR = HIDDEN OUTPUT*(1-HIDDEN OUTPUT) * (ERROR* WEIGHT TO FINALOUTPUT)

Note que, se existe várias saídas nós somamos os erros multiplicando pelo pesos das saídas, mas com XOR, nós não temos que se preocupar com isso. Finalmente, ajustamos os pesos recebidos na camada escondida com HIDDENERROR.

DELTAWEIGHT = LEARNINGCONSTANT * HIDDENERROR * INPUT NEURON
NEWWEIGHT = OLDWEIGHT + DELTAWEIGHT

E é isso! A chave aqui é compreender que devemos primeiro calcular o erro de saída da rede inteira. E, em seguinda, usar esse erro para determinar quanto cada conexão ao longo do caminho de volta para o início contribui para esse erro, e ajustar os pesos de acordo.

Aqui está a função para executar todos estes cálculos:

public double train(double[] inputs, double answer) {
  double result = feedForward(inputs);
 
  double deltaOutput = result*(1-result) * (answer-result);
 
  // Isso é fácil b/c só temos uma saída
  ArrayList connections = output.getConnections();
  for (int i = 0; i < connections.size(); i++) {
    Connection c = (Connection) connections.get(i);
    Neuron neuron = c.getFrom();
    double output = neuron.getOutput();
    double deltaWeight = output*deltaOutput;
    c.adjustWeight(LEARNING_CONSTANT*deltaWeight);
  }
 
  // Ajusta pesos escondidos
  for (int i = 0; i < hidden.length; i++) {
    connections = hidden[i].getConnections();
    double sum  = 0;
    for (int j = 0; j < connections.size(); j++) {
      Connection c = (Connection) connections.get(j);
      // Primeiro soma todas as conexões para OUTPUTS*deltaOutput (só uma saída)
      if (c.getFrom() == hidden[i]) {
                sum += c.getWeight()*deltaOutput;
      }
    }
    // Então ajusta os pesos nos próximos
    for (int j = 0; j < connections.size(); j++) {
      Connection c = (Connection) connections.get(j);
      if (c.getTo() == hidden[i]) {
        double output = hidden[i].getOutput();
        double deltaHidden = output * (1 - output);
        deltaHidden *= sum;   // Será a soma para todas as saídas se mais de uma saída
        Neuron neuron = c.getFrom();
        double deltaWeight = neuron.getOutput()*deltaHidden;
        c.adjustWeight(LEARNING_CONSTANT*deltaWeight);
      }
    }
  }
 
  return result;
}
Posted in Algoritmo, Inteligência Artificial, Rede Neural at abril 23rd, 2010. 2 Comments.

 Assinar RSS Feed