Professional Documents
Culture Documents
1. Introduo
Eu tenho interesse em inteligncia artificial e vida artificial faz tempo e j li a maioria dos livros
populares impressos sobre o assunto. Eu desenvolvi uma compreenso da maioria dos tpicos, mas ainda
assim, as redes neurais sempre pareciam ser incompreensveis para mim. Certo, eu podia explicar suas
arquiteturas, mas como elas realmente trabalhavam e como eram implementadas... Bem, isso era um
completo mistrio para mim, tanto quanto a mgica para a cincia. Eu comprei vrios livros sobre o
assunto, mas a maioria abordava o assunto de ponto um ponto de vista muito matemtico e acadmico e
muitos poucos mostravam usos prticos ou exemplos. Por muito tempo eu arranhei minha cabea e
esperei pelo dia em que teria compreenso o bastante para experiment-las eu mesmo.
Esse dia chegou um tempo mais tarde quando - sentado em uma tenda nos planaltos da Esccia,
lendo um livro - eu tive um insight repentino. Esse foi um daqueles fantsticos momentos de heureca e
embora a Esccia seja um belo lugar, eu no podia esperar para pegar um computador e experimentar o
que eu tinha aprendido. Para minha surpresa a primeira rede do neural que programei funcionou
perfeitamente e eu no tive mais problemas com elas. Eu ainda tenho uma grande vontade de aprender,
redes neurais um assunto enorme, mas eu espero que possa compartilhar conhecimento e entusiasmo o
bastante para voc comear a us-las em seus pequenos projetos. De muitas maneiras os campos da IA e
A-Life so muito excitantes de trabalhar. Eu penso nestes assuntos como os exploradores da antiguidade
deviam olhar todos aqueles vastos espaos vazios nos mapas. H tanto para aprender e descobrir.
Antes de voc comear este assunto, por favor, garanta que voc entende como usar algoritmos
genticos tambm. Isso necessrio, pois sero usados algoritmos genticos para desenvolver os pesos
da rede neural no cdigo do projeto no fim deste tutorial. Leia e entenda o contedo do captulo
anterior para isso.
Eu comearei descrevendo o que realmente uma rede do neural e que a sua arquitetura, ento
eu explicarei um pouco da teoria sobre como ns usamos elas, mas tentarei usar o menos de matemtica
possvel. (Ter que compreender algo de matemtica impossvel de se evitar, alm disso, quanto mais
fundo voc entrar neste tpico, mais matemtica voc vai ter que aprender). Finalmente, ns nos
divertiremos um pouco. Eu mostrarei um pequeno projeto e explicarei sua programao etapa por etapa.
Essa ser a ltima fase deste tutorial, onde eu espero que voc tenha o mesmo sentimento de "heureca"
para redes neurais como eu tive na velha Esccia chuvosa. At ento, somente fique sentado, absorva e
seja paciente.
O fonte C++ para o tutorial e um executvel pr-compilado podem ser encontrados em:
http://www.ai-junkie.com/files/smart_sweepers.zip
Um leitor, Sam Corder, converteu o cdigo para VB NET. Voc pode baixar esse fonte e executvel em:
http://www.ai-junkie.com/files/AntsVBdotNet.zip
Um leitor, Chris Reitzel, converteu o cdigo para DELPHI. Voc pode baixar esse fonte e executvel em:
http://www.ai-junkie.com/files/Smart_Sweepers_Delphi.zip
Cada entrada no neurnio possui um peso associado a ela como ilustrado pelos crculos vermelhos
com um w (de weight, peso). Um peso simplesmente um nmero de ponto flutuante que ajustado
quando a rede treinada. Os pesos na maioria das redes neurais podem ser tanto negativos quanto
positivos, portanto eles influenciam para excitar ou inibir cada entrada. Quando cada entrada entra no
ncleo (crculo azul) ela multiplicada por seu peso. O ncleo ento soma esses novos valores de entrada
para que eles faam a ativao (outra vez um nmero de ponto flutuante que pode ser negativo ou
positivo). Se a ativao maior que um valor baliza vamos usar o nmero 1 como exemplo o
neurnio emite um sinal de sada. Se a ativao menor que 1, o neurnio emite zero. Isso tipicamente
chamado de funo de step (degrau) (d uma espiada no seguinte diagrama e adivinhe o porqu).
a = x1w1+x2w2+x3w3... +xnwn
Felizmente a uma maneira mais rpida de escrever isso usando a letra maiscula grega sigma , a
qual o smbolo que os matemticos usam para representar a somatria.
Para esclarecer mais o que isso significa eu poderia escrever em cdigo. Assumindo que
q h um
array de entradas e pesos j inicializados como x[n] e w[n], ento:
double activation = 0;
for (int i=0; i<n; i++)
{
activation += x[i] * w[i];
}
Entendeu? Agora lembre que se a ativao > baliza, produzimos uma sada 1 e se a ativao <
baliza, emitimos 0.
Deixe-me
me ilustrar tudo que vimos at aqui com um diagrama.
Por favor, garanta que voc sabe exatamente como se calcula o valor de ativao antes de
continuar.
Cada entrada enviada para todos os neurnios da camada oculta (hidden layer) e ento cada
neurnio da camada oculta fica conectado a todos os neurnios da prxima camada. Pode haver inmeras
camadas ocultas dentro de uma rede feedforward, mas uma em geral o bastante para a maioria dos
problemas com que voc ir se deparar. Tambm pode haver qualquer nmero de neurnios em cada
camada, isso depende totalmente do problema. Agora voc pode estar se sentindo um pouco tonto com
toda essa informao, ento e acho que a melhor coisa a fazer neste ponto dar a voc um exemplo do
mundo real no qual uma rede neural pode ser usada, na esperana que eu possa fazer os seus prprios
neurnios dispararem!
Voc pode j saber que um uso popular das redes neurais o reconhecimento de caracteres. Assim
vamos projetar uma rede neural que detectar o nmero 4. Dados um painel feito de uma grade de luzes
que podem ser ligadas ou desligadas, queremos que nossa rede neural deixe-nos saber se ela acha que v
o caractere 4. O painel um quadrado de oito por oito clulas como este:
Ns poderamos projetar uma rede neural que aceitasse o estado do painel como uma entrada e
emitisse uma sada de 1 ou zero. Um 1 indica que ela acha que o caractere 4 est sendo mostrado e 0 se
ela acha que ele no est sendo mostrado. Assim a rede neural ter 64 entradas, cada uma representando
uma clula em particular no painel e uma camada oculta com um nmero de neurnios (mais desta vez)
todos emitindo sua sada para apenas um neurnio na camada de sada. Eu espero que voc possa
imaginar essa imagem na sua cabea, pois no deu para desenhar todos esses crculos e linhas para voc.
Uma vez que a rede neural tenha sido criada ela precisa ser treinada. Uma maneira de se fazer isso
inicializar a rede neural com pesos aleatrios e ento aliment-la com uma srie de entradas que
representaro, neste exemplo, as diferentes configuraes do painel. Para cada configurao fazemos um
teste para ver qual a sada e ajustamos os pesos de acordo a ela, assim se o que se v parece um nmero
4 a sada deve ser 1, para o resto a sada deve ser zero. Este tipo de treinamento chamado de
aprendizado supervisionado (supervised learning) e os dados que alimentamos a rede so chamados de
conjunto de treinamento (training set). H muitas maneiras diferentes de se ajustar os pesos, a mais
comum para este tipo de problema chamada de propagao reversa (backpropagation). Eu no irei me
aprofundar mais neste tutorial, pois mostrarei a voc uma maneira completamente diferente de treinar
redes neurais sem nenhuma superviso (e dificilmente alguma matemtica ufa!).
Se voc pensou isso, voc pode aumentar as sadas desta rede neural para 10. Dessa maneira a
rede pode ser treinada para reconhecer todos os dgitos de 0 at 9. Aumentando mais e mais, ela pode ser
treinada para reconhecer o alfabeto tambm!
Voc est comeando a compreender as redes neurais agora? Eu espero que sim. Mas mesmo que
voc no tenha entendido tudo completamente, j a hora de voc comear a ver algum cdigo.
Como voc pode ver um mostrador muito simples. Os varredores de minas so as coisas que
parecem tanques e as minas terrestres so representadas pelos pontos verdes. Assim que um varredor de
mina encontra uma mina, ela removida e outra mina posicionada aleatriamente em algum lugar do
mundo, dessa forma garantindo que sempre haver uma quantia constante de minas terrestres no
mostrador. Os varredores de minas desenhados em vermelho so melhores varredores que o programa
desenvolveu at o momento.
Mas como que uma rede neural ir controlar o movimento dos varredores? Bom, da mesma
maneira que em tanque de verdade, os varredores so controlados pelo ajuste da velocidade de uma
esteira esquerda e uma esteira direita. Ao se aplicar vrias foras no lado esquerdo e no direito de um
varredor, podemos dar a ele uma capacidade completa de movimento. Assim a rede neural precisa de
duas sadas, uma para designar a velocidade da esteira esquerda e outra para designar a velocidade da
esteira direita.
O pensamento mais inquietante que voc pode ter agora como que raios poderemos aplicar
foras diversas quando tudo que discutimos at aqui foram redes binrias que produziam sadas de 1s e
0s. O segredo para isso que em vez de usarmos uma simples funo de ativao step (baliza), ns
usaremos uma que suaviza a sada de cada neurnio para produzir uma curva simtrica. H vrias funes
que fazem isso, ns usaremos uma chamada funo sigmoid. (sigmoid ou sigmoidal apenas uma
maneira elegante de dizer que algo tem uma forma de S).
Essa equao pode parecer intimidadora para alguns, mas ela simples na realidade. O e uma
constante matemtica de aproximadamente 2.7183, o a a ativao no neurnio e o p um nmero que
controla a forma da curva. O p em geral ajustado como 1.0.
Essa funo extraordinria e til para toda sorte de diferentes usos, pois ela produz uma sada
como esta:
Quanto mais baixo o valor de p, mais a curva se assemelha a funo step.. Note tambm como essa
curva sempre centrada em 0.5. Os valores de ativao negativos produziro um resultado menor que 0.5
e valores de ativao positivos produziro um resultado maior que 0.5.
Assim, para obter uma sada continuamente nivelada entre 0 e 1 nos
os nossos neurnios,
neurnios temos que
colocar a soma das entradas x pesos na funo sigmoidal e pronto. Agora que as sadas j foram tratadas,
como sero as entradas?
Eu escolhi ter quatro entradas. Duas delas representando um vetor que aponta para a mina mais
prxima e outras duass representando a direo para onde o varredor est apontando. Esse vetor representa
a frente do varredor. Essas quatro entradas do ao crebro do varredor sua rede neural tudo que ele
precisa saber para se orientar na direo das minas.
Esta no a hora e nem o lugar para explicar o que so vetores, assim se voc no os entende, eu
sugiro que pare e os estude antes de continuar. Leia o Captulo 1 para isso.
os nossas entradas e sadas, como ser(o) a(s) camada(s) oculta(s)? Como
Agora de definimos
fazemos para definir quantas camadas teremos e quantos neurnios haver em cada camada? Bom, isso
uma matria de apostas e voc ter que desenvolver um tino
6. A classe CNeuralNet
Vamos iniciar com a classe da rede neural, CNeuralNet. Queremos que essa classe seja flexvel
para que possa ser usada em outros projetos e seja o mais simples possvel. Precisamos que ela esteja
estar apta a conter uma rede neural com qualquer quantidade de entradas e sadas, bem como qualquer
quantidade de neurnios em qualquer quantia de camadas ocultas. Assim sendo, como fazer isso? Bom,
primeiro precisamos definir as estruturas de um neurnio e de uma camada de neurnios. Vamos dar uma
olhada na definio dessas estruturas... primeiro, o neurnio:
struct SNeuron
{
//o nmbero de entradas em um neurnio
int m_NumInputs;
//os pesos de cada entrada
vector<double> m_vecWeight;
//construtor
SNeuron(int NumInputs);
};
Isso muito simples, apenas precisamos armazenar quantas entradas cada neurnio tem e um
std::vector de doubles que armazenar o peso de todas elas. Lembre, h um peso para cada entrada em
um neurnio. Quando um objeto SNeuron criado, todos os pesos so inicializados com valores
aleatrios.
Nota de programao
O std::vector parte da STL e uma classe feita para manipular arrays dinmicos. Um vetor
criado como visto anteriormente. Os elementos so adicionados pelo uso do mtodo push_back().
Assim:
#include<vector>
std::vector<int> MeuPrimeiroVetor;
for (int i=0; i< 10; i++)
{
MeuPrimeiroVetor.push_back(i);
Cout << endl << MeuPrimeiroVetor [i];
}
Ele toma o nmero de entradas que entraro no neurnio como argumento e ento cria um vetor
de pesos aleatrios. Um peso para cada entrada.
O que foi que eu ouvi voc dizer? H um peso extra a! Bom, estou feliz que voc tenha percebido
isso, pois esse peso extra muito importante, mas para explic-lo eu terei de usar mais um pouco de
matemtica. Lembre que a ativao era a soma de todas as entradas x pesos e a sada do neurnio
dependia se essa ativao excedia ou no um valor baliza (t)? E isso podia ser representado em equao
na forma de:
struct SNeuronLayer
{
//the number of neurons in this layer
int m_NumNeurons;
//the layer of neurons
vector<SNeuron> m_vecNeurons;
SNeuronLayer(int NumNeurons, int NumInputsPerNeuron);
};
Como voc pode ver, isto apenas agrupa um monte de neurnios em uma camada. A classe
CNeuralNet muito mais interessante, assim vamos seguir e ver sua definio:
class CNeuralNet
{
private:
int m_NumInputs;
int m_NumOutputs;
int m_NumHiddenLayers;
int m_NeuronsPerHiddenLyr;
//storage for each layer of neurons including the output layer
vector<SNeuronLayer> m_vecLayers;
public:
CNeuralNet();
//have a guess... ;0)
void CreateNet();
//gets the weights from the NN
vector<double> GetWeights()const;
//returns the total number of weights in the net
int GetNumberOfWeights()const;
//replaces the weights with new ones
void PutWeights(vector<double> &weights);
//calculates the outputs from a set of inputs
vector<double> Update(vector<double> &inputs);
//sigmoid response curve
inline double Sigmoid(double activation, double response);
};
A maioria disso auto-explicativa. O trabalho principal feito pelo mtodo Update. Nele
passamos nossas entradas para a rede neural como um std::vector de doubles e obtemos a sada como
outro std::vector de doubles. Este na verdade o nico mtodo que usaremos depois da classe
CNeuralNetwork ser inicializada. Apenas podemos trat-la como uma caixa preta, alimentando-a com
dados e obtendo a sada como por mgica. Vamos dar uma olhada mais de perto nesse mtodo:
vector<double> CNeuralNet::Update(vector<double> &inputs)
{
//stores the resultant outputs from each layer
vector<double> outputs;
int cWeight = 0;
//first check that we have the correct amount of inputs
if (inputs.size() != m_NumInputs)
{
//just return an empty vector if incorrect.
return outputs;
}
//For each layer....
for (int i=0; i<m_NumHiddenLayers + 1; ++i)
{
if ( i > 0 )
{
inputs = outputs;
}
outputs.clear();
cWeight = 0;
//for each neuron sum the (inputs * corresponding weights).Throw
//the total at our sigmoid function to get the output.
for (int j=0; j<m_vecLayers[i].m_NumNeurons; ++j)
{
double netinput = 0;
int NumInputs = m_vecLayers[i].m_vecNeurons[j].m_NumInputs;
//for each weight
for (int k=0; k<NumInputs - 1; ++k)
{
//sum the weights x inputs
netinput += m_vecLayers[i].m_vecNeurons[j].m_vecWeight[k] *
inputs[cWeight++];
}
Depois deste mtodo checar a validade do vetor de entrada, ele entra em um loop que examina
uma camada por vez. Para cada camada, ele passa atravs dos neurnios dela e soma todas as entradas
multiplicadas pelos pesos correspondentes. O ltimo peso adicionado em cada neurnio o vis (lembre
que o vis simplesmente um peso que sempre multiplicado por -1.0). Esse valor ento colocado na
funo sigmoidal para resultar na sada desse neurnio e ento esta adicionada a um vetor que ir
alimentar a prxima iterao do loop e assim por diante at se chegue na sada da rede.
Os outros mtodos na CNeuralNet so usados principalmente pela classe do algoritmo gentico
para obter ou trocar os pesos de uma rede.
7. A classe CGenAlg
Esta a classe do algoritmo gentico. Se voc seguiu o captulo anterior, voc entender bem
como ela funciona. H, entretanto, uma diferena com a classe CGenAlg desta vez, pois usaremos vetores
de nmeros reais em vez de sequncias de binrios.
A rede neural codificada ao se ler todos os pesos da esquerda para direita e a partir da primeira
camada oculta, debaixo para cima, se armazenando todos esses pesos em um vetor. Ento se um rede se
parece a isto:
O vetor pode ser: 0.3, -0.8, -0.2, 0.6, 0.1, -0.1, 0.4, 0.5. (Note que no estamos levando em conta
o vis, apenas os pesos como mostrado).
Agora podemos usar o cruzamento e a mutao normalmente, apenas com uma diferena: a taxa
de mutao para algoritmos genticos que usam nmeros reais muito maior... um valor entre 0,05 e 0,2
o recomendado.
Antes de eu mostrar a voc a definio da classe CGenAlg, deixe-me rapidamente mostrar a voc a
estrutura do genoma:
struct SGenome
{
vector <double>
vecWeights;
double
dFitness;
SGenome():dFitness(0){}
SGenome( vector <double> w, double f): vecWeights(w), dFitness(f){}
//overload '<' used for sorting
friend bool operator<(const SGenome& lhs, const SGenome& rhs)
{
return (lhs.dFitness < rhs.dFitness);
}
};
//generation counter
int m_cGeneration;
void Crossover(const vector<double>
const vector<double>
vector<double>
vector<double>
&mum,
&dad,
&baby1,
&baby2);
popsize,
MutRat,
CrossRat,
numweights);
//-------------------accessor methods
vector<SGenome> GetChromos()const{return m_vecPop;}
double AverageFitness()const{return m_dTotalFitness / m_iPopSize;}
double BestFitness()const{return m_dBestFitness;}
};
Quando um objeto CGenAlg criado, o nmero de pesos em cada rede neural dos varredores
passado para ele, junto com o tamanho da populao total. O construtor inicializa a populao inteira com
pesos aleatrios e ento cada cromossomo alocado ao seu respectivo crebro de varredor usando o
mtodo CNeuralNet::PutWeights.
Os varredores esto prontos para a ao!
Esta primeira parte do comando if roda todos os varredores atravs de uma gerao (uma gerao
consiste no CParams::iNunTicks em quantia de ciclos do computador), atualizando suas redes neurais e
posies de acordo. Se uma mina-terrestre encontrada, ela removida e a classificao de aptido do
varredor aumentada em 1. A mina ento trocada por outra que posicionada aleatoriamente.
//Another generation has been completed.
//Time to run the GA and update the sweepers with their new NNs
else
{
//increment the generation counter
++m_iGenerations;
//reset cycles
m_iTicks = 0;
//run the GA to create a new population
m_vecThePopulation = m_pGA->Epoch(m_vecThePopulation);
//insert the new (hopefully)improved brains back into the sweepers
//and reset their positions etc
for (int i=0; i<m_NumSweepers; ++i)
{
m_vecSweepers[i].PutWeights(m_vecThePopulation[i].vecWeights);
m_vecSweepers[i].Reset();
}
}
return true;
}
O comando else executado no fim de cada gerao. esse trecho de cdigo que coleta todos os
cromossomos dos varredores e classificaes de aptides e envia essa informao para o algoritmo
gentico. O AG ento faz suas coisas, passa os novos pesos de volta e ento os coloca nos crebros da
nova gerao de varredores. Tudo resetado um novo ciclo comea as ser executado da mesma
maneira.
Essa funo Update itera infinitamente at que voc decida que os varredores desenvolveram um
comportamento interessante o bastante. Isso em geral leva por volta de cinqenta geraes.
Apertando a tecla F quanto o programa est sendo executado, colocar o programa em modo de
tempo acelerado e voc ver um simples grfico do progresso da populao.
Quando voc viu o cdigo, possivelmente a coisa mais gritante que voc notou foi que o operador
de cruzamento simples usado aqui no muito eficiente. Voc sabe por qu? Voc pode projetar um
operador de cruzamento mais eficiente?
possvel se projetar uma rede neural que use menos entradas e neurnios ocultos. O quo
pequena voc pode fazer uma rede que ainda se desenvolva com um comportamento efetivo?