Pages

Artigo MLP - Multi Layer Perceptron com GUI e múltiplas saídas

Thursday, January 29, 2009

Olá pessoal,

Neste post, estarei anunciando uma versão "melhorada" do meu Perceptron de múltiplas camadas (Multi-Layer Perceptron - MLP). Decidi adicionar algumas funcionalidades novas e fazer alguns ajustes. Listo-as abaixo:
  • Uma representação visual da rede neural em funcionamento. (Era péssimo interpretar em console os resultados)
  • Habilidade de tratar saídas múltiplas da rede (vários outputs)
  • Opção de importação de dados a partir de um arquivo.
  • Habilidade de definir a sua arquitetura de rede com apenas uma linha de código
  • Melhor estruturação dos componentes da rede (Melhora da Clareza e visualização do código e compreensão das responsabilidades de cada classe)
A fim de ajustar e testar esta nova rede neural, decidi usar uma nova base de dados diferente das usadas nos posts anteriores. Utilizei um conjunto de dados referente às espécies de plantas com flores (Iris Plants), bastante utilizado na literatura de reconhecimento de padrões, disponível no site UCI Machine Learning Repository. O conjunto de dados contem três classes com cinquenta instâncias cada uma, o qual cada classe refere-se a uma espécie de planta Íris (Iris plant). Uma das classes é lineramente separável das outras duas, enquanto as outras duas não são linearmente separáveis uma da outra. Para utilizar estes dados, salve a página web em formato de texto com extensão .csv . Após salvo o arquivo, abra-o com o Excel ou outra ferramenta similar da sua preferência e adicione três colunas à direita, preenchendo com os respetivos valores: 0,1 or 0,1,0 ou 1,0,0 , dependendo das espécies de planta. Exclua a coluna com o nome das espécies e salve o arquivo. Pronto, você já tem o arquivo que será usado como entradas para o treinamento da sua rede neural. A imagem abaixo ilustra visualmente as saídas da rede neural, onde a mesma converge para uma solução.

Tela final após treinamento da rede neural


Decidi exibir também um pequeno diagrama de classes, onde descreverei abaixo cada classe individualmente, devido à grande quantidade de código.


Diagrama de Classes

Diferentemente do meu projeto do perceptron anterior, nesta versão utilizarei componentes gráficos que ilustrarão visualmente o funcionamento da rede durante seu treinamento. Utilizei o framework WxPython para esta tarefa, reconhecido pela facilidade e rapidez na criação de aplicativos python com GUI. Faça o download do instalador do framework de acordo com a versão do Python instalada na sua máquina e cheque se o mesmo foi instalado com sucesso através do comando no console do python: "import wx".

Vamos aos componentes do projeto:

Graph.py
Este é o container responsável pelo ponto de partida de execução do aplicativo e pela visualização gráfica do treinamento da rede neural. Para que a funcionalidade gráfica funcione, é necessário que sua rede neural tenha na sua penúltima camada de neurônios apenas 2 dimensões (2 neurônios). Embora pareça uma restrição, isso permite que seja possível a visualização gráfica da rede neural durante no seu treinamento, especialmente quando estamos refinando a rede com ajuste de parâmetros. Outro fator importante é que podemos visualizar quando o treinamento começa a ficar preso em um mínimo local (o erro começa a estagnar). Desenvolvi um mecanismo que caso você deseje reiniciar o treinamento da rede, basta apenas apertar a barra de espaço. Com certeza, que com visualização gráfica do funcionamento da sua rede, você não vai querer mais voltar para o console com números aleatórios.



##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- The Container for display the network #
# -- Version: 0.1 - 24/02/2009 #
##################################################

import wx
from Network import Network
import time

class Graph(wx.Frame):

#Class Constructor
#This method initializes all properties of this class.
#@param self:The object pointer to itself.
#@param parent: The parent of the frame
#@param id: Identification of the frame
#@param title: The title of the frame
def __init__(self,parent=None,id=-1,title=None):

#Replace with your network dimensions.
self._architecture = (4,4,2,3)

#Replace with your file location.
self._file = "iris.csv"

#The number of iterations
self._iterations = 5000

#Iteration
self._iteration = 0

#Initialize the network
self._network = Network(self._architecture,self._file)

#Instance colourMap
self._brushes = ["red","green","blue"]

#Initialize the GUI
self.initialize(parent,id,title)


#This method initializes all properties of the frame.
#@param self: The object pointer to itself.
#@param parent: The parent of the frame
#@param id: Identification of the frame
#@param title: The title of the frame
def initialize(self,parent,id,title):
wx.Frame.__init__(self,parent,id,title,size=(400,400),style=wx.SYSTEM_MENU|
wx.CAPTION| wx.CLOSE_BOX)

self._panel = wx.Panel(self, size=(400, 400))
self._panel.SetBackgroundColour("white")
self._panel.Bind(wx.EVT_KEY_DOWN, self.on_KeyDown)
self._panel.Bind(wx.EVT_PAINT, self.on_Paint)
self._panel.SetFocus()

self.Centre()
self.Fit()

#Drawing area handler
#@param self: The object pointer to itself.
#@param event: The event to be handled
def on_Paint(self,event):
#establish the painting surface
dc = wx.PaintDC(self._panel)
#Get the training error
error = self._network.train()
#Update the Plot area
self.updatePlotArea(dc,error)
#increments the iteration
self._iteration+=1

time.sleep(0.2)
#Updates the screen
if error > 0.1 and (self.iteration < self._iterations):
self.Refresh()


def updatePlotArea(self,dc,error):
#Update the title frame
strl = "Iteration=%d Error=%.2f" % (self._iteration,error)
self.SetTitle(strl)
#Clear the painting area
dc.Clear()

#Draw the instances
for point in self._network.getPoints2D():
dc.SetBrush(wx.Brush(self._brushes[int(point[2])],wx.SOLID))
dc.DrawRectangle((point[0]*395),(point[1]*395),5,5)

#Draw the HyperPlanes
for line in self._network.getHyperPlanes():
a = -line[0] / line[1]
c = -line[2] / line[1]
left = wx.Point(0,int(c*400))
right = wx.Point(400,int((a+c)*400))

dc.SetPen(wx.Pen('gray',1))
dc.DrawLinePoint(left,right)




#Key event handler
#@param self: The object pointer to itself.
#@param event: The event to be handled
def on_KeyDown(self,event):
keycode = event.GetKeyCode()
if keycode == wx.WXK_SPACE:
self._network.initialise()
self._iteration = 0
event.Skip()

#main Logic

app = wx.PySimpleApp()
frame1 = Graph(title='PyMLP 0.1')
frame1.Center()
frame1.Show()
app.MainLoop()



Network.py
Esta classe é responsável pela lógica da rede neural. A diferença principal entre essa rede neural MLP da outra rede apresentada em posts anteriores , é que essa pode tratar múltiplas saídas. Adicionei também um parâmetro que define a estrutura da arquitetura de rede usando uma tupla de inteiros. Então, por exemplo, se tivermos (4,4,2,3) significa que a rede possui 4 neurônios de entradas, a primeira camada intermediária com 4 neurônios, a segunda com 2 neurônios e por fim a camada de saída com 3 neurônios.


##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- The Multi-Layer Perceptron #
# -- Version: 0.1 - 25/02/2009 #
##################################################

"""
This is a Multi-Layer Perceptron class
with BackPropagation algorithm implemented.
"""
import random
import csv
from Layer import Layer
from Pattern import Pattern
import math

class Network(object):

#Class Constructor
#This method initializes all properties of this class.
#@param self: The object pointer to itself.
#@param architecture: Defines the network architecture using a tuple.
#@param file: File that contains the collection of training patterns
def __init__(self,architecture=None,file=None):

#Number of neurons in each layer
self._dimensions = architecture
#The set of the layers that compose the network
self._layers = None
#List of training patterns
self._patterns = None
#Initialize the network
self.initialise()
#Load the patterns from the file
self.loadPatterns(file)

#Initialize the network based on its architecture.
#@param self: The object pointer to itself.
def initialise(self):
self._layers = []
self._layers.append(Layer(self._dimensions[0]))
for i in range(1,len(self._dimensions)):
self._layers.append(Layer(self._dimensions[i],self._layers[i-1],random.Random()))

#Returns the input layer
#@param self: The object pointer to itself.
#@return: Returns the input Layer
def getInputLayer(self):
return self._layers[0].getLayer()

#Returns the output layer
#@param self: The object pointer to itself.
#@return: Returns the output Layer
def getOutputLayer(self):
return self._layers[len(self._layers)-1].getLayer()

#Propagates the perceptron and calculate the output.
#@param pattern: The pattern that will be feed at the network
def activate(self, pattern):
for i in range(len(self.getInputLayer())):
#for each input neuron set the respective input.
self.getInputLayer()[i].setOutput(pattern.getInputs()[i])

for i in range(1,len(self._layers)):
#Propagate through the network (hidden neurons).
for neuron in self._layers[i].getLayer():
neuron.activate()


#Do the training of the perceptron network.
#@param self: The object pointer to itself.
#@return: The calculated global error
def train(self):
error = 0.0
for pattern in self._patterns:
#Feed the network with the pattern
self.activate(pattern)
for i in range(len(self.getOutputLayer())):
#Calculates the error.
delta = pattern.getOutputs()[i] - self.getOutputLayer()[i].output()
#Propagates the error to get the feedback
self.getOutputLayer()[i].errorFeedback(delta)
#Evaluates the global error
error += math.pow(delta,2)
#Adjust the network weights
self.adjustWeights()
return error

#Adjust the network weights.
#@param self: The object pointer to itself.
def adjustWeights(self):
#Adjust the weights from the hidden neurons (retro-propagation).
for i in range(len(self._layers)-1,0,-1):
for neuron in self._layers[i].getLayer():
neuron.adjustWeights()

#Get the coefficients for display the planes
#@param self: The object pointer to itself.
#@return: Returns the coefficients of the lines
def getHyperPlanes(self):
lines = []
for neuron in self.getOutputLayer():
lines.append(neuron.getHyperPlane())
return lines

#Get the coordinates for display the patterns
#@param self: The object pointer to itself.
#@return: Returns the coordinates of the points
def getPoints2D(self):
penultimate = len(self._layers)-2
if len(self._layers[penultimate].getLayer()) != 2:
raise Error, "Penultimate layer must be 2D for graphing"
points = []
for i in range(len(self._patterns)):
self.activate(self._patterns[i])
point = []
point.append(float(self._layers[penultimate].getLayer()[0].output()))
point.append(float(self._layers[penultimate].getLayer()[1].output()))
if len(self.getOutputLayer()) > 1:
point.append(self._patterns[i].getMaxOutput())
else:
point.append(self._patterns[i].getOutputs()[0] >= 0.5 and 1 or 0)
points.append(point)
return points

#Load the patterns from an input file
#@param self: The object pointer to itself.
#@param file: The file path with the patterns
def loadPatterns(self,file):
self._patterns = []
#open the file
file_handler = csv.reader(open(file,"rb"),dialect='excel',delimiter=',')
for line in file_handler:
#Append the patterns
self._patterns.append(Pattern(line,len(self.getInputLayer()),len(self.getOutputLayer())))


Neuron.py
Esta classe representa o neurônio, e abriga a lógica e ativação do treinamento da rede. Bom frisar os parâmetros lambda e learningRate. Eles precisam ser ajustados conforme o refinamento da sua rede até a solução desejada.

##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- Simple Neuron of the network #
# -- Version: 0.1 - 25/02/2009 #
##################################################

import math
from Weight import Weight

#Snippet Neuron

class Neuron:

#Class Constructor
#This method initializes all properties of this class.
#@param self: The object pointer to itself.
#@param layer: The Input layer that will be connected to the Hidden Neuron.
#@param random: A random number.
def __init__(self, *pargs):

#Set of weights to inputs
self._weights = None
#Sum of inputs
self._input = 0.0
#Steepness of sigmoid curve
self._lambda = 5
#Bias value.
self._bias = 0.0
#Sum of error
self._error = 0.0
#Learning rate.
self._learningRate = 0.01
#Preset value of neuron.
self._output = None

if pargs:
#Each hidden neuron must be full-connected with all input neurons.
inputs,rnd = pargs
self._weights = []
for input in inputs.getLayer():
#New weight for each neuron.
w = Weight()
w.input = input
#Initializes with a random number.
w.value = rnd.random()* 2 - 1
self._weights.append(w)

#Set the output of the neuron.
#@param self: The object pointer to itself.
#@param value: The value to be injected at the network
def setOutput(self,value):
self._output = value

#Linear combination implementation of the perceptron.
#@param self: The object pointer to itself.
def activate(self):
self._input = 0.0
self._error = 0.0
#Calculates the input of the hidden neuron that receives the output from
#the input neuron.
for w in self._weights:
self._input += w.value * w.input.output()

#Activation function of the perceptron.
#@param self: The object pointer to itself.
def output(self):
if self._output != None:
return self._output
return 1 / (1 + math.exp(-self._lambda * (self._input + self._bias)))

#Calculates the error (feedback).
#@param self: The object pointer to itself.
def errorFeedback(self, delta):
if self._weights:
self._error += delta
for w in self._weights:
w.input.errorFeedback(self._error * self._derivative() * w.value)

#The derivative of activation function.
#@param self: The object pointer to itself.
def _derivative(self):
return self.output() * (1 - self.output())

#Adjust the weights connected to the neuron.
#@param self: The object pointer to itself.
def adjustWeights(self):
for w in self._weights:
w.value += self._error * self._derivative() * self._learningRate * w.input.output()
self._bias += self._error * self._derivative() * self._learningRate

#Get the coefficients for drawing the line
#@param self: The object pointer to itself.
def getHyperPlane(self):
line = []
line.append(self._weights[0].value)
line.append(self._weights[1].value)
line.append(self._bias)
return line


Layer.py
Esta classe encapsula uma coleção de neurônios e representa uma camada da rede neural.

##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- Simple Layer of the network #
# -- Version: 0.1 - 25/02/2009 #
##################################################


#Snippet Layer

from Neuron import Neuron

class Layer:

#Class Constructor
#This method initializes all properties of this class.
#@param self: The object pointer to itself.
#@param size: The number of Neurons that composes the layer.
#@param layer: The Input layer that will be connected to the Hidden Neuron.
#@param random: A random number.
def __init__(self,size, *pargs):

self._base = []

if pargs:
#The hidden layer is full-connected with the input layer.
#So, for each neuron, all input neurons are passed as parameter.
lyer,rnd = pargs
for i in range(size):
self._base.append(Neuron(lyer,rnd))
else:
for i in range(size):
self._base.append(Neuron())


#Get the collection
#@return: base (list with all neurons in the layer)
def getLayer(self):
return self._base


Weight.py
Esta classe abriga as informações referentes ao neurônio e seu valor numérico de peso associado. Representa os "pesos" da rede neural pelos quais conectam os neurônios uns aos outros.


##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- Simple Weight of the network #
# -- Version: 0.1 - 25/02/2009 #
##################################################


#Snippet Weight

class Weight:

#Class Constructor
#This method initializes all properties of this class.
def __init__(self):
#Neuron related to this weight.
self.input = None
#The value of the weight.
self.value = None


Pattern.py
Esta classe representa o padrão a ser apresentado à rede provido a partir de um arquivo de entrada.

##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- Sample of a pattern #
# -- Version: 0.1 - 25/02/2009 #
##################################################


class Pattern(object):

#Class Constructor
#This method initializes all properties of this class.
#@param self: The object pointer to itself
#@param line: The patterns
#@param inputDims: The number of network inputs
#@param outputDims: The number of network outputs
def __init__(self,line,inputDims,outputDims):
#The total of patterns must match with the network architecture
if len(line) != (inputDims + outputDims):
raise Error, "Input does not match network configuration"
self.inputs = []
for i in range(inputDims):
self.inputs.append(float(line[i]))
self.outputs = []
for j in range(outputDims):
self.outputs.append(float(line[j+inputDims]))


#get the Max value of the outputs
#@param self: The object pointer to itself.
#@return: The index of the max Value
def getMaxOutput(self):
item = -1
max = -100000
for i in range(len(self.outputs)):
if self.outputs[i] > max:
max = self.outputs[i]
item = i
return item

#get the Inputs set
#@param self: The object pointer to itself.
#@return: The inputs set
def getInputs(self):
return self.inputs


#get the Outputs set
#@param self: The object pointer to itself.
#@return: The outputs set
def getOutputs(self):
return self.outputs



Pronto, agora é só usar a rede aqui apresentada e fazer os testes com o conjunto de dados apresentado acima ou com um da sua preferência. Para fazer o download do código acima apresentado, clique aqui.

Espero que esse tutorial tenha sido útil para o aprendizado das redes neurais. Quaisquer dúvidas ou sugestões, só lançar comentários. Ah claro, o código acima ainda pode ter alguns bugs de desempenho ou visualização, mas que serão melhorados em futuras versões! Fiquem de olho 0_0 .

Agradeço,

e até a próxima!

Marcel P. Caraciolo

Quantos neurônios/camadas escondidas são necesários para uma rede neural ?

Wednesday, January 21, 2009

Neste post, estarei falando falando um pouco sobre uma das questões mais presentes nos círculos de estudo sobre redes neurais: Quantos neurônios que compõem a camada escondida são necessárias para solucionar o meu problema ? E quantas camadas escondidas são necessárias ?
Uma pesquisa rápida que fiz por alguns materiais, e pela minha experiência com alguns projetos utilizando redes neurais, sugiro que sejam o mínimo possível.

Na verdade, não há nenhum embasamento matemático que forneça uma resposta definitiva para estas questões. Obtemos essas respostas na base de tentativa e erro.

Vamos a um pequeno problema. Se usarmos poucos neurônios na camada escondida, a rede neural é incapaz de modelar dados mais complexos, resultando em uma baixa capacidade de generalização ('underfitting'). Se muitos neurônios forem utilizados, o treinamento da rede irá se tornar excessivamente longo e a rede pode sofrer problemas de sobreajuste ('overfitting') resultando na perda da capacidade preditiva da rede, pois observa-se pequenos desvios de previsão para os dados usados na fase de treino, mas grandes desvios quando novos dados de entrada são utilizados. Dados com ruídos ou redundantes podem provocar a falha da rede na convergência de uma solução genérica.

E em relação ao número de camadas escondidas ? Para a maiora dos problemas, uma camada escondida é suficiente. Entretanto, se seus dados contêm descontinuidades, uma camada escondida adicional pode ajudar. É importante notar que redes neurais com 2 camadas intermediárias pode aproximar funções de quaisquer tipos ou formas, então não há uma razão plausível para usar mais de duas.

Anteriormente, citei que o número de neurônios ou camadas escondidas é baseado sobre a técnica de "tentativa e erro". Mas, o que isto significa ? O processo utilizado por alguns estudiosos na área, é determinar o número de neurônios escondidos baseado na premissa de que rede neurais com muitos neurônios nas camadas intermediárias irão convergir. Logo, o objetivo é tentar encontrar a menor rede neural possível que convirja e refinar a rede a partir desta encontrada. Segue um pequeno algoritmo, para melhor esclarecimento:
  1. Inicie com um neurônio na camada escondida, semelhante ao Perceptron de uma camada.
  2. Inicie o treinamento da rede.
  3. Se a rede falha em convergir para uma solução genérica depois de um tempo considerável, reinicie o treinamento da rede por até 10 vezes. Isto garante que a rede não caiu em um mínimo local.
  4. Se a rede neural ainda falha para convergir para uma solução genérica (ótima), adicione mais um neurônio na camada intermediária, e retorne ao passo 2.
  5. Se chegou aqui, significa que a rede convergiu. Note que o número de neurônios da camada escondida aqui encontrada, é armazenado como o número máximo necessário.
  6. Remova um neurônio da camada escondida e reinicie o treinamento.
  7. Repita o ciclo de treinamento (passos 02 e 03), até que a rede convirja para uma nova solução ou por algum critério de parada definido por você.
  8. Se a rede convergir, então você reduziu seu número máximo necessário. Armazene o número de neurônios da camada escondida e retorne ao passo 5.
Usando este processo descrito acima, descreveremos um exemplo de problema solucionado, indicando o número necessário de neurônios escondidos.

Problemas de classificação
Um dos problemas clássicos de classificação binária é o problema do XOR (ou paridade) . Neste tipo de problema estamos apenas interessados nas quinas do hiper-cubo, e não no espaço interno que o compõe. A tabela abaixo ilustra o conjunto de dados de entrada e saída para um problema XOR de 3 dimensões. Esses pontos podem ser mapeados em um cubo, conforme a figura abaixo.
xyzout
0000
0011
0101
0110
1001
1010
1100
1111

Usando um Perceptron de múltiplas camadas (como o apresentado no post passado), obtemos rapidamente o número de neurônios escondidos para solucionar o problema do XOR variando o numéro de bits de entrada de 1 a 8. Os resultados são ilustrados no gráfico abaixo:




Espero que essa breve explicação possa contribuir com algumas técnicas específicas meios de encontrar a estrutura apropriada para sua rede neural. Não há nada comprovado teoricamente, mas serve como um procedimento simples de achar a estrutura de rede que satisfaça os objetivos do problema a ser solucionado. De fato, é mais arte do que ciência!

[Artigo] Redes Neurais Perceptron de Múltiplas Camadas MLP

Sunday, January 11, 2009

No último artigo publicado, eu apresentei como a rede neural Perceptron de múltiplas camadas (MLP) pode ser utilizada para resolver problemas não linearmente separáveis. Em um breve exemplo, exibi como foram calculados manualmente os pesos da rede neural. Neste artigo, será apresentado o algoritmo de treinamento denominado Retro-propagação (Back Propagation), que irá ser utilizado para gerar os pesos de forma automática.

O algoritmo de retro-propagação (Back Propagation) se tornou o algoritmo de treinamento de redes neurais mais utilizado, e tem sido estudado pela comunidade científica de inteligência artificial desde a década de 70. Ele é utilizado em frameworks de construção de redes neurais, como por exemplo o Matlab.

O príncipio do algoritmo back propagation é relativamente fácil de entender, embora o embasamento matemático por trás dele possa parecer um pouco complexo. Os passos do algoritmo são:


  1. Inicialize os pesos da rede neural com valores aleatórios pequenos.
  2. Apresente um padrão (dado) para a camada de entrada da rede neural.
  3. Propague o padrão de entrada pelas camadas intermediárias da rede neural a fim de calcular a saída da sua função de ativação.
  4. A diferença entre a saída desejada e a saída da função de ativação será usada para calcular o erro de ativação da rede.
  5. Ajuste os pesos, através do neurônio de saída da rede a fim de reduzir o erro de ativação para o padrão de entrada apresentado.
  6. Propague o valor do erro de volta (retorno) para cada neurônio da camada intermediária, considerando a proporção de sua contribuição no cálculo do erro de ativação da rede.
  7. Ajuste os pesos, agora propagando o erro pelos neurônios da camada intermediária a fim de reduzir a sua contribuição para o erro calculado para o padrão de entrada apresentado à rede.
  8. Repita os passos 2 a 7 para cada padrão de entrada do conjunto de dados apresentado à rede.
  9. Repita o passo 8 até que a rede esteja treinada.
É importante atentar que cada padrão de entrada é apresentado à rede em turnos, ajustando aos poucos os pesos da rede, antes de mover para o próximo padrão de entrada. Se deixarmos a rede corrigir perfeitamente os erros antes de mover para o próximo padrão de entrada, a rede neural perderá a capacidade de generalização a fim de encontrar uma solução que satisfaça todo o conjunto de dados de entrada.

A grande "jogada" do algoritmo de treinamento acontece no passo 6, quando ele determina a quantidade de erro que deve ser propagada de volta (daí o nome retro-propagação) para cada neurônio das camadas intermediárias. Uma vez que o valor do erro for calculado, o treinamento pode continuar conforme descrito semelhante ao algoritmo do perceptron de uma camada.

Para ilustrar como o valor do erro é calculado, considere a rede neural abaixo:




Multi Layer Perceptron

Logo, se usarmos estas variáveis:

output_o = Resposta da função de ativação do neurônio de saída.
error_o = Erro calculado pelo neurônio da camada de saída (output).
error_h = Erro no neurônio da camada intermediária.
weight_ho = Peso conectando o neurônio da camada intermediária com o neurônio da camada de saída.

O erro propagado de volta para o neurônio da camada intermediária é calculado pela fórmula:



error_h = error_o * Derivative(output_o) * weight_ho

Para uma explicação detalhada sobre como foi obtido a derivada da função de ativação do sigmóide, ver o artigo sobre funções de ativação.


Diferentemente da rede Perceptron de uma camada, o treinamento do Perceptron de múltiplas camadas (MLP) com o algoritmo Back-Propagation não garante uma solução final correta, mesmo que uma seja possível. Isto acontece porque durante o treinamento, o mesmo pode ficar preso em uma região de erro de mínimo local. Há diversas estratégias para evitar a ocorrência deste problema, que serão discutidas em detalhes em um próximo artigo. Para agora, reiniciar o treinamento novamente é suficiente para pequenas redes neurais.

Será utilizado para este artigo o mesmo problema de classificação proposto no último artigo.





Os padrões de entrada são representados pelos pontos (vermelhos e azuis) plotados no gráfico acima.

Segue abaixo a implementação em Python de uma rede MLP, ainda sem otimizações.

O código referente ao peso (weight) conectado aos neurônios:


##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- Simple Weight of the network #
# -- Version: 0.1 - 13/01/2009 #
##################################################


#Snippet Weight

class Weight:

#Class Constructor
#This method initializes all properties of this class.
def __init__(self):
#Neuron related to this weight.
self.input = None
#The value of the weight.
self.value = None




Segue o código referente ao Neurônio da rede:




##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- Simple Neuron of the network #
# -- Version: 0.1 - 13/01/2009 #
##################################################

import math
from Weight import Weight

#Snippet Neuron

class Neuron:

#Class Constructor
#This method initializes all properties of this class.
#@param layer: The Input layer that will be connected to the Hidden Neuron.
#@param random: A random number.
def __init__(self, *pargs):

#Set of weights to inputs
self._weights = None
#Sum of inputs
self._input = 0.0
#Steepness of sigmoid curve
self._lambda = 6
#Bias value.
self._bias = 0.0
#Sum of error
self._error = 0.0
#Learning rate.
self._learningRate = 0.5
#Preset value of neuron.
self._output = None

if pargs:
#Each hidden neuron must be full-connected with all input neurons.
inputs,rnd = pargs
self._weights = []
for input in inputs.getLayer():
#New weight for each neuron.
w = Weight()
w.input = input
#Initializes with a random number.
w.value = rnd.random()
self._weights.append(w)

#Set the output of the neuron.
def setOutput(self,value):
self._output = value

#Linear combination implementation of the perceptron.
def activate(self):
self._input = 0.0
#Calculates the input of the hidden neuron that receives the output from
#the input neuron.
for w in self._weights:
self._input += w.value * w.input.output()

#Activation function of the perceptron.
def output(self):
if self._output != None:
return self._output
return 1 / (1 + math.exp(-self._lambda * (self._input + self._bias)))

#Calculates the error (feedback).
def errorFeedback(self, input):
weight = None
for w in self._weights:
if w.input == input:
weight = w
break
return self._error * self._derivative() * weight.value

#The derivative of activation function.
def _derivative(self):
return self.output() * (1 - self.output())

#Adjust the weights connected to the neuron.
def adjustWeights(self,value):
self._error = value
for w in self._weights:
w.value += self._error * self._derivative() * self._learningRate * w.input.output()
self._bias += self._error * self._derivative() * self._learningRate



Os neurônios estão organizados em camadas. Segue abaixo a abstração de uma camada em python:




##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- Simple Layer of the network #
# -- Version: 0.1 - 13/01/2009 #
##################################################


#Snippet Layer

from Neuron import Neuron

class Layer:

#Class Constructor
#This method initializes all properties of this class.
#@param size: The number of Neurons that composes the layer.
#@param layer: The Input layer that will be connected to the Hidden Neuron.
#@param random: A random number.
def __init__(self,size, *pargs):

self._base = []

if pargs:
#The hidden layer is full-connected with the input layer.
#So, for each neuron, all input neurons are passed as parameter.
lyer,rnd = pargs
for i in range(size):
self._base.append(Neuron(lyer,rnd))
else:
for i in range(size):
self._base.append(Neuron())


#Get the collection
#@return: base (list with all neurons in the layer)
def getLayer(self):
return self._base




E o código referente à toda rede neural: Camadas de neurônios interconectados por pesos:




##################################################
# #
# Copyright 2009 -Marcel Pinheiro Caraciolo- #
# Email: caraciol@gmail.com #
# #
# -- Multi Layer Perceptron Neural Net #
# -- Version: 0.1 - 12/01/2009 #
##################################################


#Snippet MLP

import random
import math
from Layer import Layer
from Neuron import Neuron


"""
This is a Multi-Layer Perceptron class
with BackPropagation algorithm implemented.
"""
class MLP:


#Class Constructor
#This method initializes all properties of this class.
#@param iterations: The number of iterations.
#@param architecture: (numberOfHiddenNeurons,numberOfInputNeurons)
#@param patterns: Collection of training patterns
def __init__(self,patterns,iterations=5000, architecture=(2,1)):

#Number of hidden neurons, Number of input Neurons
self._hiddenDims,self._inputDims = architecture
#Current training iteration
self._iteration = 0
#Maximum number of iterations before restart training
self._iterations = iterations
#Set of hidden neurons
self._hiddenLayer = None
#Set of input neurons
self._inputLayer = None
#List of training patterns
self._patterns = list(patterns)
#Output neuron
self._output = None
#Random number generator
self._rnd = random.Random()

#Initialize the network
self. _initialize()


#Initialize the network based on its architecture.
def _initialize(self):
#First Layer (2 neurons representing x and y)
self._inputLayer = Layer(self._inputDims)
#Hidden Layer (2 neurons representing h1 and h2)
self._hiddenLayer = Layer(self._hiddenDims, self._inputLayer, self._rnd)
#Initialize the output neuron (o).
self._output = Neuron(self._hiddenLayer,self._rnd)
#Reset the iterations.
self._iteration = 0
print "Network Initialized..."

#Adjust the network weights.
def _adjustWeights(self, delta):
#Adjust the weights from the output neuron.
self._output.adjustWeights(delta)
#Adjust the weights from the hidden neurons (retro-propagation).
for neuron in self._hiddenLayer.getLayer():
#Output neuron connected to the respective neuron.
neuron.adjustWeights(self._output.errorFeedback(neuron))


#Propagates the perceptron and calculate the output.
def _activate(self, pattern):
for i in range(len(pattern[0])):
#for each input neuron set the respective input (x,y).
self._inputLayer.getLayer()[i].setOutput(pattern[0][i])

for neuron in self._hiddenLayer.getLayer():
#Propagate through the network (hidden neurons).
neuron.activate()

#Calculate the output from the output neuron
self._output.activate()

#Calculates the output of the network (output neuron)
return self._output.output()

#Do the training of the perceptron network.
def train(self):
error = 1.0
while error > 0.1:
error = 0.0
for pattern in self._patterns:
#Calculates the error.
delta = pattern[1] - self._activate(pattern)
#Adjust the weights of the network
self._adjustWeights(delta)
#Evaluates the global error
error += math.pow(delta,2)
print "Iteration %d Error: %f" % (self._iteration, error)
self._iteration += 1
#Restarts the training/Initialization.
if self._iteration > self._iterations:
self._initialize()

#Test the network after trained.
def execute(self,pattern):
return self._activate(pattern)

#Generates random numbers.
def arange(self,start,stop=None,step=None):
if stop is None:
stop = float(start)
start = 0.0
if step is None:
step = 1.0
cur = float(start)
while cur <= stop: yield cur cur+=step


Execute o programa acima usando:



#main Logic

#Load sample input patterns
inputs = [
[(0.10, 0.03), 0], [(0.11, 0.11), 0],[(0.11, 0.82), 0],
[(0.13, 0.17), 0],[(0.20, 0.81), 0],
[(0.21, 0.57), 1],[(0.25, 0.52), 1],[(0.26, 0.48), 1],
[(0.28, 0.17), 1],[(0.28, 0.45), 1],[(0.37, 0.28), 1],
[(0.41, 0.92), 0], [(0.43, 0.04), 1], [(0.44, 0.55), 1],
[(0.47, 0.84), 0], [(0.50, 0.36), 1], [(0.51, 0.96), 0],
[(0.56, 0.62), 1], [(0.65, 0.01), 1], [(0.67, 0.50), 1],
[(0.73, 0.05), 1], [(0.73, 0.90), 0], [(0.73, 0.99), 0],
[(0.78, 0.01), 1], [(0.83, 0.62), 0], [(0.86, 0.42), 1],
[(0.86, 0.91), 0], [(0.89, 0.12), 1], [(0.95, 0.15), 1],
[(0.98, 0.73), 0] ]

#Initializes the Multi-Layer Perceptron
mlp = MLP(inputs,iterations=2000, architecture=(2,2))
#Train the network whit the training inputs.
mlp.train()
#Display network generalization
print ""
print "X, Y, Output"
for pattern in inputs:
#Calculate output.
result = mlp.execute(pattern[:-1])
print "%f %f %s" % (pattern[0][0],pattern[0][1],result)



Quando o aplicativo for executado, ele utilizará o algoritmo de retro-propagação (back propagation) para treinar a rede neural, reiniciando caso ela fique presa em algum erro mínimo local. Uma vez totalmente treinada, será possível testar a rede neural com novas entradas (pontos do gráfico) desconhecidas e assim observar a capacidade de generalização da rede, classificando corretamente tais pontos após o aprendizado da rede.

Pode-se observar que é simples modificar o código a fim de inserir mais dimensões (nesse gráfico são 2 dimensões, mas pode ser 3 dimensões) e aumentar o número de neurônios da camada escondida, variando as variáveis X e Y. Não esquecer claro de adicionar as colunas extras ao conjunto de entradas apresentado à rede. Logo, a rede pode ser usada para solucionar problemas de alta complexidade.

Donwnload do código aqui.

Aplicações desta rede neural poderão ser abordadas em futuros artigos, exibindo o poder dessas redes em solucionar problemas do mundo real. Fiquem antenados!

Parallel Parking: Uso de sensores inteligentes para estacionamento do automóvel

Thursday, January 8, 2009

Inteligência Artificial aplicada com agentes inteligentes : Sensores.

Um exemplo ? Imagine a possibilidade de seu carro automaticamente estacionar numa vaga de estacionamento. Não, nem precisa imaginar. Veja!

A Ford apresentou um sistema de estacionamento automático de automóveis (Parallel Parking). Muito Interessante! Segue o vídeo abaixo:




Fonte: MobilidadeTudo

[Artigo]: Introduzindo a rede neural Multi-Layer-Perceptron (MLP)

Sunday, January 4, 2009

Neste post, apresentaremos alguns conceitos iniciais relacionados à solução de problemas linearmente não-separáveis com o Multi Layer Perceptron (MLP).

Será utilizado o mesmo tipo de problema que foi apresentado no post sobre a rede neural Perceptron. Pode-se notar que para esse conjunto de dados, não há como separá-los linearmente (classificação). Será necessário mais de 1 reta para uma classificação correta.


Linearly Non-Separable

A rede neural utilizada para resolver este problema está representada graficamente abaixo. Nesta rede neural, adicionaremos agora um coeficiente linear (bias) a mesma. Sem este coficiente, qualquer reta (ou hiper-plano) deverá passar sempre pela origem (0,0), o que é uma restrição considerada desnecessária.


Multi Layer Perceptron

O papel dos dois neurônios escondidos é dividir o espaço do conjunto de dados de entrada em diversas partições. Para atingir tal objetivo, baseado na figura acima, poderiamos plotar 2 retas arbitrárias, conforme o gráfico abaixo.

Input Space

Os valores dos coeficientes das equações correspondem diretamente aos pesos da rede neural (semelhante ao treinamento da rede Adaline). Entretanto, primeiramente é necessário fazer alguns ajustes nas equações a fim de que todos os termos estejam do mesmo lado, i.e. 0 = y + 0.36x - 0.85, etc. Logo, os pesos da rede serão estes a seguir:

wx1 = 0.36
wy1 = 1
wb1 = -0.85

wx2 = 1.92
wy2 = 1
wb2 = -0.70

Embora, nós tenhamos os pesos (valores) de uma rede que possa corretamente classificar os pontos de um problema não linearmente- separáveis, deparamos com algums problemas.

Como são 2 equações da reta, é necessário converter ou transformar o espaço amostral da entrada em um espaço que se possa ser linearmente separável a fim de que o neurônio de saída possam realizar corretamente a tarefa de classificação das entradas. Para isto, utilizamos a função de ativação apresentada em post anterior: Função Sigmóide com a introdução agora de uma variável lambda (λ).

Sigmoid Function

def function(output):

return 1 / (1 + math.exp(-lambda * output))


O Lambda permite que o gradiente da função sigmóide desloque o espaço dos dados entrada gerando um espaço distorcido. Usando, por exemplo, o lambda com valor igual a 6, o espaço de entrada poderia ser visto assim:

Feature Space with Lambda


Agora, os dados de entrada estão linearmente separáveis, e consequentemente o neurônio de saída pode classificar corretamente. Os valores representados pela equação equivalem diretamente aos pesos obtidos pelo treinamento da rede. Logo, os pesos na camada de saída da rede seriam:


wx1o = -0.73
wyh2o = 1
bo = 0.21

Embora, tenhamos obtido todos os pesos manualmente, é possível obter os valores desses pesos automaticamente. Para isto, é necessário de um algoritmo de treinamento, assim como foram deduzidos com o Adaline e Perceptron.
No próximo post, será introduzido o algoritmo de retro-propagação (Back-Propagation) com um exemplo prático desenvolvido em python.

Até lá,

Marcel P. Caraciolo