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

2 comments: