Universidade do Algarve Faculdade de Ciências e Tecnologia

Propaganda
Universidade do Algarve
Faculdade de Ciências e Tecnologia
Inteligência Artificial
(2006-2007– 2º Semestre)
Trabalho prático 2:
Uma rede neuronal para o quebra-cabeças da
AMI
Discentes:
Filipe Silva Pereira nº 17144
Renato Miguel Santos nº 24143
1. Introdução
Este trabalho tem o objectivo de implementar um perceptrão multi-camada, com
aprendizagem por retropropagação do erro, para dotar um resolutor do quebra-cabeças
da AMI com aprendizagem.
O programa recebe da linha de comandos a topologia pretendida para a rede neuronal,
definindo-se assim o número de entradas, saídas, camadas e de neurónios por camada
Um exemplo da linha da topologia introduzida na linha de comandos seria:
>java redeNeuronal 2 2 1
Assim, a rede teria 3 camadas (uma de entrada, uma escondida e uma de saída) 2
entradas, 1 saída e um total de 3 neurónios (2+1, na camada de entrada não existem
neurónios).
Para facilitar a leitura dos dados obtidos e a partir deles criar os gráficos pedidos no
enunciado do trabalho, o programa guarda num ficheiro "dados.csv" os dados relativos
aos pesos iniciais e finais, a margem de erro utilizada, o valor do momentum e o racio
utilizado para actualizar o eta, num ficheiro "etas.csv" guarda todos os etas de cada
época e num ficheiro “emq.csv” guarda a média do erro médio quadrado em que cada
linha nestes dois últimos ficheiros corresponde a uma época. As referências para as
páginas web pesquisadas encontram-se na bibliografia.
2. Modelo da rede neuronal
A rede neuronal implementada é composta por várias camadas, a primeira contém as
entradas da rede e as seguintes contêm neurónios. Cada neurónio pode ter várias
entradas e a cada entrada é associado um peso. Para se calcular a saída do neurónio fazse o somatório da multiplicação de cada entrada com o seu peso correspondente e
aplica-se uma função sigmoide. Neste trabalho utilizámos a fórmula 1/( 1+e-x ) para o
cálculo da sigmoide. A saída então calculada será uma entrada de cada neurónio da
camada seguinte. Na camada de saída temos apenas um neurónio cuja saída corresponde
à saída da rede.
Figura 1. Modelo de uma rede neuronal
3. Algoritmo utilizado para aprendizagem
Como foi referido na introdução, a rede tem aprendizagem através do algoritmo de
retropropagação do erro que, resumidamente, é usado para aprender os pesos ideais para
a rede utilizando a descida do gradiente para minimizar o erro quadrado entre os valores
de saída e os valores esperados.
A rede é inicializada com pesos aleatórios e dado um determinado conjunto de entrada
calcula-se a saída. Com o valor da saída calcula-se o quadrado da diferença em relação
ao valor esperado, achando-se assim o erro médio quadrado. Depois percorre-se a rede
desde a camada de saída até à camada de entrada, actualizando-se os pesos de cada
neurónio. Isto é feito até que seja encontrada uma condição de paragem. A condição de
paragem de paragem por nós implementada consiste em arredondar os valores de saída
e os valores esperados do conjunto de treino para depois compará-los. Quando todos
forem iguais a rede pára.
Neste algoritmo são utilizados alguns termos que é importante ter a noção do que são:
Gradiente do erro – o gradiente do erro é determinado através da derivada da sigmoide
multiplicado pelo erro da saída do neurónio.
Erro da saída – diferença entre o valor obtido e o valor esperado
Eta – corresponde ao ritmo de aprendizagem
Rácio do eta – valor entre 1 e 1.99 utilizado para acelerar o ritmo de aprendizagem.
Momentum – É uma constante utilizada para acelerar o processo de aprendizagem e
reduzir os saltos que a curva de aprendizagem dá ao longo do processo.
Delta dos pesos – A variação dos pesos é obtida através da expressão:
Momentum vezes Delta pesos anterior mais eta vezes valor de saída do neurónio vezes
gradiente do neurónio. [4] página 185 fórmula (6.17).
4. Desenvolvimento
Tal como no primeiro trabalho, a linguagem de programação utilizada foi o Java. A rede
neuronal é usada como uma função de avaliação heurística para o primeiro trabalho mas
neste relatório explicamos apenas as novas classes implementadas, já que as outras
classes utilizadas já estão explicadas no primeiro relatório.
Este trabalho tem 3 novas classes:
redeNeuronal:
Implementa a rede neuronal com aprendizagem por retropropagação do erro.
Tem as seguintes variáveis globais:
public double[] entradasNaRede;
public double[] valEsperadoRede;
public Neuronio[][] rede;
private double[][][] cuboDePesos;
private double momentum;
private double emq; //erro médio quadrado
private double racioEta;
private double eta;
static int div; // o valor desta variável é utilizado para dividir os valores dos conjuntos
de teste e de treino de modo a que estes estejam entre 0 e 1.
Usa vários métodos para actualizações de valores usados no algoritmo de
aprendizagem:
private void actualizaDeltasR()
- Percorre a rede no fim para o ínicio e actualiza cada um dos delta através da fórmula:
momentum*deltaPeso + (eta*entrada*gradiente)
private void actualizaPesosR()
- Adiciona a cada peso o seu delta
public void actualizaEta(int epoca, int aumenta, int diminui, double []
buffer, double mediaEMQ, double racio)
O método de actualização dos etas utilizado foi retirados de [4] paginas 186 e 187, e é
feita de 6 em 6 épocas, onde ao fim das quais existe uma actualização de eta*rácioEta
ou de eta/racioEta. Aumenta quando a maioria dos elementos do buffer só aumenta ou
só diminui. Caso contrario reduz-se o ritmo de aprendizagem.
private void setEmq()
- O erro médio quadrado é calculado através da potência de 2 do erro da rede e divide-se
esse valor por 2.
Depois vêm os métodos de inicialização:
public redeNeuronal (int[] topologia)
- Inicializa a rede com a topologia pretendida, alocando espaço para os neurónios a
inicializar e guardando os valores dos pesos na variável global cuboDePesos. É aqui
também que se define os valores do rácio, do momentum e do eta.
Para a inicialização dos pesos temos dois métodos:
private void atribuiPesosR(double [][][]pesos)- Aqui são inicializados com
valores pré-definidos.
private void atribuiPesosR()
- Aqui são inicializados aleatoriamente.
public void resetDeltaPesos()
- faz reset ao deltaPesos
public void correRedeFrente (double[] entradas, double[] valEsperados)
- Corre a rede para a frente, fazendo todos os cálculos para se obter os valores de saída
Usa uma função da classe neuronio para efectuar os cálculos a cada neurónio da rede.
private double decTrunc(double valor, int decimal)
- Criámos esta função para "truncar" alguns valores demasiados extensos a uma
determinada casa decimal
private static double[] heur( String str )
- Esta função recebe um estado do quebra-cabeças e retorna um array com o valor de
várias heurísticas aplicadas ao estado recebido.
public static void main(String[] args)
- Recebe como argumento a topologia pretendida, define-se aqui os conjuntos de treino
e de teste e chama-se as respectivas funções para treinar e testar a rede.
public static char[][] strToMat (String str)
- Criámos esta função para transformar uma string numa matriz com as dimensões
correspondentes
public void treinoDaRede(double[][][] conjTreino, FileWriter ficheiro,
FileWriter ficheiroEtas)
- Treina a rede com o conjunto de treino definido. Percorre várias épocas até atingir a
condição de paragem. Em cada época actualiza os pesos e o eta de forma a diminuir o
erro. Guarda ainda os dados relativos ao treino em ficheiros
private boolean terminaAprendizagem(double[][][] conjTreino, int epoca)
- Implementa a condição de paragem do treino da rede que consiste na comparação
entre os valores obtidos (doubles) arredondados para o inteiro mais próximo e o valor
esperado. Caso todos os elementos do conjunto de treino menos um sejam iguais, o
treino pára. Optou-se por retirar um elemento desta forma porque tipicamente a rede
demora muito mais tempo a aprender o último elemento, assim de acordo com a
inicialização dos pesos o elemento mais difícil de aprender é ignorado.
public void testeDaRede(double[][][] conjTeste)
- Testa a rede a partir de um dado conjunto de teste.
No fim da classe existem algumas funções para guardar os dados em ficheiros
consoante o tipo de dados:
private static FileWriter criaCSV(String ficheiro)
private static void guardaDados(int dados, FileWriter outFile, String
nomeArray)
private static void guardaDados(double dados, FileWriter outFile, String
nomeArray)
private static void guardaDados(int[] array, FileWriter outFile, String
nomeArray)
private static void guardaDados(double[] array, FileWriter outFile, String
nomeArray)
private void guardaDadosPesos(FileWriter outFile, String nomeArray)
private void guardaDadosEtas(FileWriter outFile)
Neuronio:
Esta classe serve para implementar cada um dos neurónios com as suas características
individuais.
Características essas que são usadas como variáveis globais da classe:
private
private
private
private
private
private
private
double[] entrada;
double[] peso;
double[] deltaPeso;
double valorSaida = 0;
int camada;
int numero;
double somatorio=0;
Tem um construtor para um neurónio, recebendo a camada a que pertence, a sua posição
na camada e a topologia de rede utilizada
public Neuronio (int camad, int numer, int[] topologia)
public void atribuiPesosN()
- Atribui pesos ao neurónio a partir de um array contendo esses mesmos pesos.
private void actualizaPesosN()
- Actualiza os pesos do neurónio através do deltaPeso
private void actualizaDeltasN()
- Actualiza os deltaPesos do neurónio
public void resetDeltaPesosN()
- Coloca os deltaPesos do neurónio a zero
Para os cálculos a usar em cada neurónio utiliza-se os seguintes métodos:
private
private
private
private
void funciona(double[] entrds)
void setSomatorio()
double sigmoid()
double derivada()
HeurísticaRedeNeuronal:
Esta classe implementa uma heurística utilizando a rede neuronal criada.
Cria-se uma instância da rede com topologia introduzida explicitamente. Os pesos
introduzidos nesta instância são os que foram obtidos na aprendizagem e também são
introduzidos explicitamente. As três entradas são heurísticas implementadas no primeiro
trabalho (AnéisDe4e2, Manhattan e Quinas2).
Contém apenas uma função:
public int valor(char[][] mat, char[][] solucao)
- Corre a rede para a frente com os valores dos pesos introduzidos e o valor de saída da
rede corresponde ao valor desta heurística.
5. Testes
No fim testámos três topologias com cinco inicializações cada e apresentamos os
resultados pedidos no enunciado que são:
- O número total de pesos da rede, ou seja, a soma do tamanho dos arrays de pesos de
cada neurónio.
- O erro médio produzido que interpretámos como sendo o erro médio quadrado obtido
em cada saída na última época. No nosso caso como só temos uma saída só
apresentamos um valor.
- O número de iterações necessárias para atingir a condição de paragem, ou seja, o
número de épocas.
1ª Inicialização
2ª Inicialização
3ª Inicialização
4ª Inicialização
5ª Inicialização
Topologia: 3 4 2 1
Nº total de pesos Erro médio
29
3,87E+12
29
5,88E+11
29
3,86E+12
29
0.0012675792931943041
29
1,50E+12
Nº de Iterações
317874
167148
381978
44719
958207
1ª Inicialização
2ª Inicialização
3ª Inicialização
4ª Inicialização
5ª Inicialização
Topologia: 3 5 2 1
Nº total de pesos
Erro médio
35
0.001227750035785427
Divergiu
Divergiu
35
8,90E+11
35
0.0010200570846973598
35
6,82E+11
Nº de Iterações
26158
Divergiu
55528
57682
12355
1ª Inicialização
2ª Inicialização
3ª Inicialização
4ª Inicialização
5ª Inicialização
Topologia: 3 5 3 1
Nº total de pesos
Erro médio
42
6,09E+11
Divergiu
Divergiu
42
7,30E+11
42
6,04E+10
42
7,02E+10
Nº de Iterações
31298
Divergiu
71896
200613
3876
Com base nisto escolhemos a 4ª inicialização da topologia 3-4-2-1 como a melhor
inicialização. Assim foram os pesos finais obtidos nesta inicialização que introduzimos
na heuristicaRedeNeuronal. De seguida apresentamos os pesos iniciais e finais desta
inicialização e os gráficos correspondentes à evolução do eta e do erro médio quadrado.
Não nos foi possível apresentar a curva de evolução dos erros com o eta fixo pois à hora
da realização deste relatório ainda tínhamos a rede a correr nestas condições e tal iria
demorar ainda algumas horas a terminar.
Pesos Iniciais:
1º neurónio 0.079
2º neurónio -0.131
3º neurónio 0.305
4º neurónio 0.321
5º neurónio 0.14
6º neurónio 0.034
7º neurónio 0.333
-0.303
-0.023
0.447
0.214
0.384
-0.437
0.454
0.117
-0.492
-0.38
-0.397
0.169
-0.312
-0.004
0.373
-0.259
-0.008
-0.131
-0.416
-0.385
0.18
0.252
Pesos Finais:
1º neurónio
2º neurónio
3º neurónio
4º neurónio
5º neurónio
6º neurónio
7º neurónio
0.087
-0.083
0.834
-0.064
2.07
-2.311
2.828
0.047
-0.519
-0.186
-0.939
0.161
-0.3
-3.113
0.007
-0.299
-0.854
0.669
-0.914
0.552
-0.891
1.794
2.798
-0.126
-0.078
-0.363
0.162
-0.504
0.04
Gráfico do eta:
0,25
0,2
etas
0,15
Série1
0,1
0,05
0
1
36
71 106 141 176 211 246 281 316 351 386 421
epocas/100
Gráfico do erro médio quadrado:
0,06
0,05
EMQ
0,04
0,03
Série1
0,02
0,01
0
1
32
63
94 125 156 187 218 249 280 311 342 373 404 435
Epocas/100
6. Notas finais sobre o código feito
Este trabalho consiste de 10 ficheiros de código java:
5 deles são do primeiro trabalho e não têm qualquer alteração.
Tabuleiro.java
IdaAst.java
Heuristica.java
Quinas2.java
Manhattan.java
O ficheiro QC.java tem uma linha alterada: a linha da declaração da heuristica,
que agora é a nova heurística baseada na rede neuronal criada.
O ficheiro AneisDe4e2.java tem a heuristica corrigida.
Este segundo trabalho foi feito em 2 ficheiros: redeNeuronal.java e
HeuristicaRedeNeuronal.java . O primeiro tem 2 classes, a classe neuronio
dentro da classe redeNeuronal. O segundo é a nova heurística, que consiste na
criação de um instância de uma rede neuronal, escrevendo explicitamente no
código a topologia e os pesos aleatórios que consideramos mais adequados. O
critério escolhido foi a aprendizagem mais curta da topologia mais pequena que
obtemos.
O objectivo final é puder correr o primeiro trabalho com uma heurística bastante
mais eficiente. O trabalho que entregamos, a rede está sobre treinada, ou seja,
resolve bem, os elementos aprendidos, mas comete erros para outros valores.
A forma de correr o primeiro trabalho está explicada nesse relatório.
Existem 2 ficheiros com a função main. Ou seja, o QC.java, que é o ficheiro
relativo ao primeiro trabalho, e o ficheiro redeNeuronal.java.
Para correr o ficheiro redeNeuronal tem que se depois de compilar, escrever
como argumentos a topologia da rede:
~/redeneuronal $ javac *.java
~/redeneuronal $ java redeNeuronal 3 4 2 1
Quando se corre desta forma está-se a treinar a rede com pesos gerados
aleatoriamente.
O output que se apresenta quando se corre o programa é primeiro, o nome dos
ficheiro para os quais se está a enviar dados, e depois, de 50 em 50 épocas de
treino mostra-se quantos valores já se aprendeu, o erro médio quadrado do
valor devolvido pela rede e o valor do eta (o ritmo de aprendizagem).
7. Conclusão
Após a realização deste trabalho e dos testes realizados verificámos que a nossa rede
demora muito tempo a aprender. Isto dá-se devido às heurísticas que utilizámos, pois
com heurísticas mais adequadas teria-se convergências mais rápidas.
De notar ainda que não se convém usar uma topologia que contenha camadas com
muitos neurónios porque assim a rede fica demasiado especializada.
8. Bibliografia
[1] Ernesto Costa e Anabela Simões, Inteligência Artificial: Fundamentos e Aplicações,
FCA, Fev. 2004
[2] Stuart Russel and Peter Norvig, Artificial Intelligence: A modern approach, 2nd
edition, Pearson Education, 2003.
[3] Inteligência Artificial 2006/2007, “http://w3.ualg.pt/~jvo/ia/”
[4] Michael Negnevitsky, Artificial Intelligence: A Guide to Intelligent Systems,
Addison-Wesley, 2005
Download