sexta-feira, 17 de abril de 2015

[Python] - Implementação da rede Perceptron

Já falamos em outros posts sobre Redes Neurais, daremos continuidade ao assunto (caso tenha perdido os outros posts basta fazer uma busca no canto superior direito) dessa vez abordando a rede Perceptron.

A Perceptron é a forma mais simples de configuração de uma rede neural artificial. Caso você esteja começando seus estudos com redes neurais, é aconselhável entender ela primeiro antes de partir para o entendimento de outras redes.

A rede Perceptron tem apenas uma camada neural e somente um neurônio artificial nesta única camada, por isso ela é tão simples.

A rede Perceptron possui dois sinais de entrada (x1 e x2) e somente uma saída porque a rede é composta de apenas um neurônio. Apesar dessa rede do exemplo possuir só dois sinais de entrada, a rede Perceptron pode ser constituída de "n" sinais de entrada, isso é de acordo com o problema a ser mapeado.

Cada entrada do nosso exemplo (x1 e x2) representam informações sobre o comportamento do processo a ser mapeado, cada uma será inicialmente ponderada pelos pesos sinápticos (w1 e w2) a fim de quantificar a importância de cada uma.

O valor resultante da composição de todas as entradas devidamente ponderadas pelos seus respectivos pesos (adicionado ainda do limiar de ativação - theta), é repassado como argumento da função de ativação cujo resultado será a saída "y" produzida pela rede.

Em termos matemáticos:

u = SOMATOTIO(i=1 até n) (wi * xi) - limiar_ativacao

"n" é o número de entradas
"xi" são as entradas da rede
"wi" é o peso sináptico associado à i-ésima entrada
"limiar_ativacao" é o limiar de ativação representado por theta (é -1)

Após calcular o "u", basta submeter ele à função de ativação "g" para obter o "y":

y = g(u)

Relembrando: o "u" é chamado de potencial de ativação, é o resultado produzido pela diferença do valor produzido entre o combinador linear (somatório) e o limiar de ativação. Já o objetivo da função de ativação é limitar a saída do neurônio dentro de um intervalo de valores razoáveis a serem assumidos pela sua própria imagem funcional. O "y" é o valor final produzido pelo neurônio em relação a um determinado conjunto de sinais de entrada.

Fácil não é mesmo? Esse é o processamento interno realizado pelo Perceptron.

As funções de ativação normalmente usadas na Perceptron são a função degrau ou degrau bipolar.

A função degrau é bem simples:

g(u) = 1, se u >= 0
g(u) = 0, se u < 0

A função degrau bipolar (ou função sinal) também é muito simples:

g(u) = 1, se u >= 0
g(u) = -1, se u < 0

As entradas "xi" podem assumir quaisquer valores numéricos.

Para atualizar (ajustar) os pesos e limiar do Perceptron utiliza-se o processo de treinamento supervisionado, ou seja, para cada amostra dos sinais de entrada se tem a respectiva saída (resposta) desejada.

O Perceptron é comumente usado para classificação de padrões, como a saída pode assumir somente dois valores, então cada um desses valores está associado a uma das duas classes que o Perceptron estará identificando. Exemplo: identificar se é homem ou mulher, homem e mulher seriam as classes.

Se utilizássemos a função degrau bipolar, poderíamos dizer que a saída -1 está associada a classe A e 1 está associada a classe B. Se fosse a degrau, poderíamos dizer que 0 está associada a classe A e 1 está associada a classe B.

Se usarmos a função de ativação bipolar (sinal) temos que (considerando somente duas entradas):

y = 1, se w1 * x1 + w2 * x2 - limiar >= 0
y = -1, se w1 * x1 + w2 * x2 - limiar < 0

O Perceptron se comporta como um classificador de padrões cuja função é dividir classes que sejam linearmente separáveis.

O Perceptron consegue dividir duas classes linearmente separáveis, sendo que quando a saída for -1 significa que é da classe A e 1 indica que é da classe B.

A regra de aprendizado de Hebb é utilizada para ajustar os pesos e limiar do Perceptron.

Se a saída produzida pela rede é igual a saída desejada, então os pesos sinápticos e limiares da rede serão então incrementados (ajuste excitatório) proporcionalmente aos valores de seus sinais de entrada; caso contrário, os pesos sinápticos e limiar serão então decrementados (inibitório).

O processo descrito acima é repetido sequencialmente para todas as amostras até que a saída produzida pelo Perceptron seja igual a saída desejada de cada amostra.

Em termos de implementação, pode-se inserir o valor do limiar dentro do vetor de pesos sinápticos, pois o limiar também é uma variável que deve ser ajustada para realizar o treinamento do Perceptron:

w = w + N * (d(k) - y) * x(k)

O "w" é o vetor contendo o limiar e os pesos sinápticos.

Já o x(k) é a k-ésima amostra de treinamento.

d(k) é o valor desejado para a k-ésima amostra de treinamento.

"y" é o valor da saída produzida pelo Perceptron.

"N" é a taxa de aprendizagem da rede, ou seja, é uma constante.

O "N" é um valor que deve está compreendido entre 0 e 1, ou seja, 0 < N < 1.

A rede será considerada treinada quando inexistir erro entre os valores produzidos pela rede e os desejados.

Após o treinamento, você poderá "perguntar" à rede, ou seja, a rede poderá classificar padrões frente às novas amostras que serão apresentadas.

Antes de ver o código, é sempre bom você tentar implementar para entender melhor, essas explicações são exatamente para isso.

A implementação da rede Perceptron foi feita em Python, a seguir você poderá visualizar o código:


Para quem quiser se aprodundar mais no assunto, aconselho a ler o livro "Redes Neurais Artificiais para engenharia e ciências aplicadas" do Ivan Nunes. 

Quaisquer dúvidas deixem nos comentários, até a próxima!


6 comentários:

André Lucas Silva disse...

Olá, implementei um código baseado no exemplo do mesmo livro mas na linguagem em C (se quiser compartilho com você). Porém, já fiz diversos testes, com pesos aleatórios mas não consigo chegar em nenhuma solução correta do treinamento, mesmo após o treinamento ser efetuado em 1000 épocas.

Você tem alguns valores de peso que são atribuidos como corretos?

Marcos Castro disse...

Olá amigo, inicialmente os pesos tem que ser aleatórios, esses pesos que a rede irá ajustar a cada época. Perceba que uso pesos aleatórios (linha 33 do código).

Você poderia compartilhar o código? Utilize o http://gist.github.com pra ficar melhor de analisar, obrigado.

André Lucas disse...

Olá, segue o código abaixo, espero que compreenda. O exemplo é a aplicação são os mesmos. Porém, fiz o teste com pesos aleatórios diversas redes e não conseguir fazer a rede convergir.

https://gist.github.com/andlucsil/6b64d3139dcd62dc77c7

André Lucas disse...

Consegui solucionar com o Matlab (se quiser te passo o código também!). Os pesos que encontrei são:

-3.24
-0.9342
-3.6363
-2.1439

Marcos Castro disse...

hum, interessante, ainda não tive tempo para analisar seu código em C, irei analisar logo logo e postarei a resposta. Coloca aí o código do matlab para ajudar o pessoal :) Valeu!

André Lucas disse...

Código no Matlab:

clear all;
clc;
p = [0.1 0.5 0.6 0.3;
0.4 0.7 0.9 0.7;
0.7 0.1 0.8 0.2];
t = [0 0 1 1];
plotpv(p,t)
net = perceptron;
net = configure(net,p,t);
net.iw{1,1,1} = [0.68 -0.778 0.015];
net.b{1} = -0.241;
linehandle = plotpc(net.iw{1,1,1},net.b{1});
hold on
for cont=1:1000
net = adapt(net,p,t);
linehandle = plotpc(net.iw{1,1},net.b{1},linehandle);
drawnow;
Saida=sim(net,p)%Saida
Pesos=net.iw{1,1}%Peso das ligaçoes das entradas
PesoBias = net.b{1}%Peso da bias
end;
hold off