Treinamento de Redes Neurais com Algoritmo Genético

Acessado 2337 vezes.
Como citar esse artigo: VERTULO, Rodrigo Cesar. Treinamento de Redes Neurais com Algoritmo Genético. Disponível em: <http://labdeeletronica.com.br/noticias/treinamento-de-redes-neurais-com-algoritmo-genetico/>. Acessado em: 18/04/2024.


Atualmente existem muitas arquiteturas de redes neurais para resolverem os mais variados tipos de problemas. Contudo, quase a totalidade delas baseia-se em princípios determinados por Rosenblatt no ano de 1958 e publicados em seu livro Principles of Neurodynamics. Rosenblatt criou uma rede neural muito simples conhecida como Perceptron capaz de solucionar problemas linearmente separáveis.

De forma bastante simplista, um problema linearmente separável é aquele cujas soluções podem ser separadas por uma reta, como é demonstrado a seguir.

fig. 1 – Problemas linearmente separáveis

 

No exemplo apresentado na fig. 1 nota-se que utilizando uma reta é possível separar os quadrados dos círculos, de modo que esse é um problema de classificação passível de ser resolvido por um Perceptron já que trata-se de um problema linearmente separável.

Perceptron de Rosenblatt é constituído pelos elementos apresentados a seguir.


fig. 2 – Perceptron de Rosenblatt (fonte: Redes Neurais Artificiais – Fundamentos e Modelos. Claudio Loesch e Solange T. Sari. Editora da FURB)

 

Note na fig.2 que x1, x2, …, xn são os dados de entrada do Perceptron e que eles são inseridos no modelo por meio de uma camada formada pelos pesos w1, w2, …, wn. Há também um peso chamado w0 que não recebe nenhuma entrada xn. Após a camada contendo os pesos existe um módulo responsável por realizar o somatório de cada entrada multiplicada pelo seu peso correspondente. Como para o peso w0 não existe uma entrada xn correspondente, ele é multiplicado por 1. Para exemplificar, suponha uma rede contendo duas entradas x1 e x2. Para esse caso, o resultado obtido pelo módulo de somatório é calculado conforme pode ser observado a seguir.

s = (x1*w1) + (x2*w2) + (1 * w0)

em que s é a saída do módulo de somatório.

Após o módulo de somatório vem o módulo de ativação, cuja saída será 1 ou 0 para indicar se as entradas fornecidas para a rede geraram ou não sinal de ativação. Para ficar mais claro, se o problema a ser resolvido fosse separar quadrados e círculos como mostrado na fig.1, uma saída 0 poderia indicar que os dados apresentados na entrada representam um quadrado ou 1 se eles representam um círculo. O módulo de ativação é formado, no caso do Perceptron de Rosenblatt, por uma Função Sinal. Essa função é extremamente simples, sendo definida da seguinte forma.

y = 0 se s < 0
y = 1 se s >= 0

em que y é o resultado do módulo de ativação e é o resultado obtido no módulo de somatório.

A rede neural Perceptron criada por Rosenblatt é capaz de resolver apenas problemas linearmente separáveis por possuir apenas uma cada (a camada de entrada). Posteriormente descobriu-se que adicionando camadas extras para criar uma rede Perceptron Multicamadas problemas não linearmente separáveis também poderiam ser resolvidos por essas redes neurais.

Em uma rede neural do tipo Perceptron como a descrita anteriormente, espera-se que a partir de um conjunto de dados de entrada apresentados à mesma, ela seja capaz de fornecer em sua saída uma valor 0 ou 1 que resolva um determinado tipo de problema. Suponha uma rede neural que deva ser capaz de resolver a função lógica AND. Sabe-se que para uma função desse tipo um valor 1 é obtido somente quando todas as entradas são também 1. Veja a seguir:

 

x1 x2 Saída
0 0 0
0 1 0
1 0 0
1 1 1

 

É perfeitamente possível de se resolver esse tipo de problema utilizando-se uma rede neural do tipo Perceptron de uma camada contendo duas entradas, x1 e x2, com seus pesos w1 e w2 mais o peso adicional w0. O grande problema é determinar quais são exatamente os valores desses pesos que gerarão na saída da rede os valores 0 ou 1 corretos de acordo com as entradas x1 e x2 conforme foi apresentado na tabela anterior referente à função lógica AND. A identificação dos pesos ideais é feita por meio do que se chama de Treinamento da Rede Neural. Tradicionalmente utiliza-se uma série de operações matemáticas complexas para a determinação dos pesos da rede neural. De forma bastante simplificada essas operações consistem em apresentar um conjunto de dados de treinamento para a rede neural definindo-se incialmente os pesos com valores aleatórios. De acordo com o resultado obtido na saída de rede os pesos sofrem pequenos ajustes e os dados de entrada são novamente apresentados à rede neural. Esse procedimento de realização de pequenos ajustes nos pesos se repete até que as saídas apresentadas pela rede de acordo com os dados de entrada apresentem um um nível de acerto ideal.

Apesar do treinamento das redes neurais seguindo os procedimentos matemáticos resumidamente apresentados anteriormente serem bastante eficazes e eficientes, nesse artigo será proposta uma nova abordagem para a realização de treinamento de uma rede neural Perceptron de uma Camada. Existe um ramo da Inteligência Artificial que se concentra no desenvolvimento de algoritmos inspirados na biologia. Eles são conhecidos como algoritmos bio inspirados. Dentro dessa classe encontram-se os algoritmos evolutivos, sendo os Algoritmos Genéticos (AG) um dos mais conhecidos. Os AG são desenvolvidos a partir de conceitos forjados por Charles Darwin em 1838, quando ele desenvolveu a teoria da Seleção Natural.

Utilizando conceitos como Seleção dos mais aptos, Reprodução e Mutação, os AG são capazes de encontrar soluções quase ótimas (potencialmente ótimas) para problemas a partir da evolução de indivíduos considerados mais aptos dentro de uma população de candidatos.

As áreas de aplicação dos GA são vastas e vão desde a otimização de planos de cortes para chapas de aço, madeira, etc., até o planejamento rotas de transporte e controladores para veículos e robôs autônomos. Nesse artigo o objetivo será apresentar o desenvolvimento de um Algoritmo Genético utilizando-se a biblioteca Opensource chamada PyGARV em conjunto com a linguagem de programação Python para realizar o treinamento de uma rede Perceptron de Camada Única. Basicamente o algoritmos genético encontrará os pesos a serem utilizados na rede neural. No site da biblioteca existe uma ótima documentação explicando o funcionamento da mesma.

As observações de Charles Darwin durante sua viagem ao redor do mundo, no navio HMS Beagle, culminou com a publicação de sua obra mais importante no ano de 1859 intitulada “On the Origin of Species by Means of Natural Selection, or the Preservation of Favoured Races in the Struggle for Life” (Da Origem das Espécies por Meio da Seleção Natural ou a Preservação de Raças Favorecidas na Luta pela Vida), onde ele apresenta a Teoria da Evolução.

Darwin notou diferenças físicas em indivíduos de uma mesma espécie que ao longo do tempo se propagavam nas novas gerações daquela população. Ele teorizou que a prevalência daquelas características nos indivíduos das novas gerações se deviam ao fato delas os tornarem mais aptos a sobreviverem no ambiente em que se encontravam. Apesar dos fundamentos do mecanismo da hereditariedade terem sido apresentados por Gregor Mendel apenas em 1866, os achados de Darwin já sugeriam a existência dos mesmos.

Para nossos propósitos, que é o desenvolvimento de um Algoritmo Genético em Python para o treinamento da rede neural Perceptron, basta sabermos que os estudos de Darwin e Mendel nos levaram ao que hoje conhecemos popularmente como o princípio da “Sobrevivência dos Mais Fortes”. Fundamentando-se em elementos como Geração de uma População Inicial, Seleção dos Mais Aptos, Reprodução, Transmissão de Características e Mutação, é possível replicar muitos dos aspectos presentes nos estudos de Darwin e Mendel para a criação de soluções computacionais capazes de resolverem os mais variados tipos de problemas relacionados à busca de respostas ótimas, ou quase ótimas, para os mesmos.

É importante frisar que Algoritmos Genéticos são uma ótima alternativa para a resolução de problemas cuja solução por outros meios torna-se inviável ou complexa demais por conta do custo e tempo de processamento ou dificuldade de implementação. Existem problemas com um universo de respostas tão vasto, que encontrar a melhor solução por meios analíticos tradicionais poderia levar anos até mesmo nos mais modernos e velozes processadores. Para esses casos, encontrar uma solução quase ótima já é suficiente. Os Algoritmos Genéticos baseiam-se em métodos probabilísticos, fazendo com que não existam garantias de que eles encontrarão a melhor solução para os problemas, contudo, eles são capazes de chegarem muito perto das mesmas, ou seja, de soluções quase ótimas.

O funcionamento dos Algoritmos Genéticos seguem o fluxo operacional apresentado a seguir. Vale destacar que  objetivo aqui não será detalhar tecnicamente como cada etapa funciona, de modo que utilizaremos a biblioteca PyGARV que será responsável em implementar os mecanismos do Algoritmo Genético para resolver nosso problema.

 


fig.3  – Fluxograma de funcionamento dos Algoritmos Genéticos (fonte: http://labdeeletronica.com.br/algoritmos-geneticos-e-raspberry-pi-parte-2-principios/)

 

O objetivo do código que será apresentado será o de definir os pesos de uma rede neural Perceptron de uma Camada de modo que ela seja capaz de resolver a função lógica AND. A rede neural receberá dois valores de entrada e fornecerá uma saída como resposta respeitando a tabela verdade da lógica AND. A solução desse problema utilizando um Algoritmo Genético consistirá em definir-se a estrutura dos cromossomos da população seguindo a estrutura a seguir.

 

sw0 w0 sw1 w1 sw2 w2

 

É possível observar acima que os cromossomos são formados por seis genes que armazenam o valor numérico de cada peso e o sinal dos mesmos (positivos ou negativos). Por exemplo, para o peso w0, o primeiro gene sw0 armazena o sinal (sinal positivo ou negativo na forma 1 ou -1) e o segundo gene armazena o valor do peso w0. A mesma lógica se repete para os outros pesos da rede. Como será utilizada a biblioteca PyGARV será necessário criar os cromossomos respeitando a forma de trabalho da mesma. Nesse caso, cada gene será formado por um número inteiro de três dígitos. Para o caso dos sinais (sw0, sw1, sw2 e sw3) se o número inteiro for par, o mesmo será considerado como sendo um sinal negativo e se for ímpar como sendo um sinal positivo. Por outro lado, os valores de cada peso (w0, w1 e w2) serão divididos por 1000 a fim de se obter sempre um valor menor  que um para cada peso. No exemplo a seguir ficará mais clara essa forma de codificação dos cromossomos.

 

204 135 311 400 110 153

 

Os valores escolhidos acima foram tomados de forma aleatória apenas a título de exemplo. Nesse caso, sabemos que o primeiro gene referente a sw0 representa o valor -1, pois trata-se de um valor par. O segundo gene, referente a w0, possui o valor 0,135 já que 135/1000 = 0,135. Seguindo a mesma lógica, o terceiro gene (sw1) representa o valor 1 e o quarto gene o valor 0,400. Executando-se o mesmo procedimento para o restante dos valores chega-se a:

w0 = -0,135
w1 = 0,400
w2 = -0,153

Esses valores de pesos seriam utilizados pelo Algoritmo Genético para avaliar o quão bons eles são para resolver nosso problema.

Agora que a forma de codificação dos cromossomos do Algoritmo Genético foi explicada, será apresentado o código completo do algoritmo e logo a seguir a explicação do funcionamento do mesmo para resolver o problema da função lógica AND com a rede neural Perceptron.

from PyGARV import *
 
class RN(PyGARV):
    def __init__(self):
        super().__init__(popSize = 50, 
                         values = 6, 
                         mutationRate = 0.01,
                         fullMutation = False,
                         symmetricCut = True,
                         crossoverRate = 1,
                         elitism = 0,
                         digits = 3)
 
 
        self.dataset = [   
                        [0,0,0],
                        [0,1,0],
                        [1,0,0],
                        [1,1,1]
                       ]
 
 
    def fitness(self, chromosome):
        sw0 = -1 if chromosome[0]%2 == 0 else 1
        w0 =  sw0*chromosome[1]/1000
 
        sw1 = -1 if chromosome[2]%2 == 0 else 1
        w1 = sw1*chromosome[3]/1000
 
        sw2 = -1 if chromosome[4]%2 == 0 else 1
        w2 = sw2*chromosome[5]/1000
 
        erro = 0
        for i in self.dataset:
            somatorioEntradas = i[0]*w1 + i[1]*w2 + w0
            funcaoAtivacao = 0 if somatorioEntradas &lt; 0 else 1
            erro = erro + ((funcaoAtivacao - i[2])**2)
 
        nota = 1/(erro+0.0001)
 
        return [chromosome, nota]
 
 
    def finishedGA(self, chromosome):
        sw0 = -1 if chromosome[0]%2 == 0 else 1
        w0 =  sw0*chromosome[1]/1000
 
        sw1 = -1 if chromosome[2]%2 == 0 else 1
        w1 = sw1*chromosome[3]/1000
 
        sw2 = -1 if chromosome[4]%2 == 0 else 1
        w2 = sw2*chromosome[5]/1000
 
        print("Pesos: ", w0, w1, w2)
 
        for i in self.dataset:
            saida = i[0]*w1 + i[1]*w2 + w0
            print(i[0], i[1], 0 if saida &lt; 0 else 1)
 
 
pygarv = RN()
pygarv.runGA(500)

O Algoritmo Genético mostrada acima foi criado seguindo-se as instruções contidas no site da biblioteca PyGARV. Observa-se que cada cromossomo possuirá seis valores (values = 6), cada um contendo três dígitos (digits = 3) conforme discutido anteriormente. Os outros parâmetros do construtor foram definidos de forma empírica. No construtor também é possível notar a presença do atributo “self.dataset”. Ele consiste de uma lista contendo a tabela verdade da função lógica AND. O primeiro valor de cada elemento da lista é a entrada um, o segundo a entrada dois e o terceiro o resultado da operação lógica aplicada sobre a entrada um e dois.

Em seguida há o método “fitness” utilizado pelo Algoritmo Genético para avaliar a aptidão de cada cromossomo para resolver o problema em questão. As seis primeiras linhas de código do método realizam as operações descritas anteriormente sobre cada gene do cromossomo para obter-se os valores dos pesos w0, w1 e w2.

A próxima etapa do método consiste na criação de um laço for para iterar sobre cada elemento do “self.dataset” e aplicar as operações do Perceptron descritas no início desse artigo, ou seja, aquelas referentes ao módulo de somatório das entradas multiplicadas pelos seus pesos e em seguida a implementação da função de ativação.

O objetivo da variável erro é ter uma medida do quão próximo do resultado esperado o cromossomo foi capaz de chegar. Em outras palavras, o cromossomo possui pesos codificados em seus genes que ao serem submetidos ao módulo de somatório e ao de ativação gerará na saída da rede neural um valor 1 ou 0. Se o valor gerado na saída da rede, de acordo com as entradas fornecidas, coincidirem com a saída esperada presente em “self.dataset” o valor acumulado na variável erro será zero. Quando as saídas obtidas não baterem com as esperadas o valor de erro será positivo e maior do que zero, indicando o quão distante de valores ideias os pesos contidos nos cromossomos estão.

A medida da aptidão de um cromossomo é calculada com o uso da variável nota logo a seguir. Quanto maior for o erro menor será a nota do cromossomo, portanto, menos apto ele será para resolver o problema.

Quando o Algoritmo Genético termina seu processamento o método “finishedGA” é executado e o cromossomo presente na assinatura do mesmo representará o melhor cromossomo obtido. Os genes desse cromossomo são extraídos da forma como já foi explicada e finalmente a saída obtida pela rede Perceptron a partir de cada entrada contida em “self.dataset” é calculada. Além disso os pesos armazenados no melhor cromossomo são exibidos. As duas últimas linhas do algoritmo inicializa o Algoritmo Genético que será executado por 500 gerações.

Ao executar esse algoritmo você notará que os pesos gerados para a rede neural Perceptron de uma Camada são capazes de resolver o problema da função lógica AND. Como exercício você poderá alterar “self.dataset” para representar a função lógica OR e constatar que o algoritmo também gerará pesos capazes de resolver esse novo problema. Tanto a função AND quanto a OR são linearmente separáveis e passíveis de serem solucionadas pelo Perceptron de camada única. Por outro lado, a função lógica XOR não é linearmente separável e impossível de ser solucionada com esse tipo de rede neural. Para esse caso é preciso utilizar-se uma rede neural Perceptron de Múltiplas Camadas, mas isso é assunto para um próximo artigo.

Eletrônica Simples Para Projetos Complexos
Deixe de ser alguém que fica copiando e colando circuitos da Internet e passe a ser um projetista capaz de criar soluções inovadoras com esse poderoso componente.

Comentários