You are here

Relatório da Aula 03 (15/03/2013) SOAR - Tutorial 2

SUMÁRIO

1.  Introdução
2.  Objetivos
3.  Experimentos e Resultados
     3.1.  Atividade 1 - Exploração do jogo Eaters
     3.2.  Atividade 2 - Consolidação dos conhecimentos sobre o Soar
     3.3.  Atividade 3 - Resolução do problema da ausência de comida
     3.4.  Atividade 4 - Desafio da busca sistemática por comida
4.  Conclusão
5. Referências Bibliográficas
 


1.  Introdução

Este relatório apresenta a descrição das atividades e os resultados obtidos nos experimentos propostos na Aula 3 do Curso IA006 - Laboratório em Arquiteturas Cognitivas. Estes experimentos dão continuidade ao aprendizado da arquitetura cognitiva Soar e explorarão o controle de criaturas artificiais simples em um jogo do estilo Pacman: o Eaters.

Os objetivos gerais são apresentados na seção 2. A seção 3 traz a descrição dos experimentos realizados, seus objetivos específicos e os resultados obtidos em cada um deles. A seção 4 apresentam as conclusões obtidas a partir dos experimentos.
 


2.  Objetivos

A principal motivação dos experimentos desta aula é prover o entendimento sobre a operação dos mecanismos de estados e operadores para controlar, de fato, a criatura artificial no jogo.

Os objetivos principais destes experimentos são:

  • Criar agentes a partir de estudos de programas Soar disponíveis conforme o tutorial;
  • Fazer uso dos mecanismos estudados nas aulas anteriores para consolidação dos conhecimentos obtidos, tais como o uso das ferramentas, produção de regras, elaboração de estados, definição de operadores, preferência entre operadores, interface de entrada e saída etc.;
  • Resolver o problema do comportamento indesejado evidenciado no jogo;
  • Resolver o desafio proposto da busca sistemática por comida.

3.  Experimentos e Resultados
 

3.1.  Atividade 1 - Exploração do jogo Eaters

3.1.1. Objetivos específicos

  • Explorar o jogo Eaters disponível para entender o seu funcionamento;
  • Estudar as produções do Soar envolvidas para controle da criatura no jogo.

3.1.2. Desenvolvimento

Este experimento começa com a execução do programa Eaters. Deve-se criar uma nova criatura cujo comportamento é controlado por produções do Soar, mais especificamente, as regras do programa move-to-food.soar. A Figura 1 mostra a tela inicial do programa Eaters.

Figura 1 - Tela inicial do programa Eaters
Figura 1 - Tela inicial do programa Eaters.

Para iniciar o programa, é necessário criar uma nova criatura (agente). A criação é feita através do botão <New> na barra de botões da seção Agents. O Eaters apresenta duas formas de controle do agente: manual, cujo controle é feito pelo usuário a partir das teclas de navegação do teclado numérico, e automático a partir de um programa Soar. A Figura 2 mostra a tela para criação de um novo agente no programa Eaters.

Figura 2 - Tela para criação de um novo agente no Eaters
Figura 2 - Tela de criação de um novo agente no Eaters

Para este experimento, deve ser criado um novo agente a partir do programa move-to-food.soar. Após selecionado o programa e cridado o agente, o jogo pode ser iniciado. Para iniciar o jogo, basta clicar no botão <Run> da seção Simulation. A Figura 3 mostra a execução do Eaters com um agente controlado pelo programa move-to-food.soar.

Figura 3 - Tela do Eaters em execução com agente controlado pelo programa move-to-food.soar
Figura 3 - Tela do Eaters em execução com um agente controlado pelo programa move-to-food.soar

Ao ser carregado um programa Soar para controle do agente no Eaters, uma instância da aplicação Soar Debugger é iniciada. A execução de uma simulação no Eaters pode ser depurada no Soar Debugger para cada um dos agentes presentes no jogo. A Figura 4 mostra a saída do log na tela do Soar Debugger para o agente em execução na primeira simulação.

Figura 4 - Depuração da execução do agente no Eaters com o Soar Debugger
Figura 4 - Depuração da execução do agente com o Soar Debugger.

Quando o programa é executado, o agente procura por comida nas células adjacentes à sua posição atual. As células podem conter comida (normais ou bônus), blocos da parede, outros agentes ou vazio (quando não se tem comida, bloco ou agente). Ao ser detectada comida em alguma das células adjacentes, o agente dirige-se de modo a posicionar-se sobre a célula. Neste instante, considera-se que o agente comeu a comida e a mesma, então, desaparece dali deixando-a vazia.

Para entender como o controle do agente é feito, o código-fonte do programa move-to-food.soar deve ser estudado e analisado. A Listagem 1 mostra o código-fonte do programa move-to-food.soar

# From Chapter 6 of Soar 8 Tutorial
#
# These are the final versions of the rules.
#
# This program proposes the move-to-food operator in any direction
# that contains normal or bonus food.  If there is no food nearby, no
# instances of the operator will be proposed and the halt operator
# will be proposed.

# Propose*move-to-food*normalfood
# If there is normalfood in an adjacent cell,
#    propose move-to-food in the direction of that cell
#    and indicate that this operator can be selected randomly.

sp {propose*move-to-food
   (state <s> ^io.input-link.my-location.<dir>.content
                 << normalfood bonusfood >>)
-->
   (<s> ^operator <o> + =)
   (<o> ^name move-to-food
        ^direction <dir>)}

# Apply*move-to-food
# If the move-to-food operator for a direction is selected,
#    generate an output command to move in that direction.

sp {apply*move-to-food
   (state <s> ^io.output-link <ol>
              ^operator <o>)
   (<o> ^name move-to-food
        ^direction <dir>)
-->
   (<ol> ^move.direction <dir>)}

# Apply*move-to-food*remove-move:
# If the move-to-food operator is selected,
#    and there is a completed move command on the output link,
#    then remove that command.

sp {apply*move-to-food*remove-move
   (state <s> ^io.output-link <ol>
              ^operator.name move-to-food)
   (<ol> ^move <move>)
   (<move> ^status complete)
-->
   (<ol> ^move <move> -)}

Listagem 1 - Código-fonte do programa move-to-food.soar (Laird, 2012).

Abaixo são feitas considerações sobre os aspectos mais importantes do programa Soar da Listagem 1 que controla a agente no jogo.

O módulo Soar como um Enclave

O Soar é instanciado pelo Eaters para fazer com que um agente possa ser controlado por regras do programa move-to-food.soar. Este modo de operação é chamado de enclave. O Eaters informa ao programa Soar a posição atual do agente e um campo de visão. Da mesma forma, ele recebe do programa Soar a direção para a qual o agente deve mover-se.

A interface de comunicação entre o Eaters e o Soar é representada no Soar como descrições do vértice input-link e output-link do grafo que representa o estado da memória de trabalho do agente. A Figura 5 mostra o ciclo de decisão do Soar em que informações são passadas para o programa via input-link e recebem informações do Soar via output-link.

 

Figura 5 - Ciclo de decisão do Soar
Figura 5 - Ciclo de decisão do Soar (Laird, 2012).

 

As informações do Eaters passadas para o módulo do Soar estão representadas da seguinte forma:

  (I21 ^eater I4 ^my-location I5)
  (I4 ^direction south ^name red ^score 25 ^x 1 ^y 10)

1 Os identificadores dos objetos são gerados automaticamente pelo Soar e podem variar a cada ciclo de decisão conforme as alterações dos estados na memória de trabalho. O prefixo I do nome do identificador indica que trata-se de um atributo de IO e a numeração subsequente é de controle do Soar, podendo variar conforme os ciclos. Os identificadores só permanecem os mesmos nos casos em que os WMEs são mantidos na memória de trabalho entre os ciclos de decisão (persistência).

O objeto I5 representa o campo de visão do agente. O campo de visão é uma grade formada por 25 células, com 5 colunas e 5 linhas e centro na posição atual do agente. A Figura 6 mostra o campo de visão do agente no Eaters.

Figura 6 - Campo de visão do agente
Figura 6 - Campo de visão do agente

As informações recebidas do módulo do Soar estão representadas desta forma:

  (O1 ^direction south ^name move-to-food)

Estado Inicial

O estado inicial do agente é considerado no primeiro ciclo de decisão executado logo após o início da execução do Eaters. A Listagem 2 mostra o estado inicial da memória de trabalho.

watch 3
     0: ==>S: S1
print S1
(S1 ^epmem E1 ^io I1 ^reward-link R1 ^smem S2 ^superstate nil ^type state)

Listagem 2 - Estado inicial do programa move-to-food.soar.

O primeiro de ciclo de decisão é executado logo que o Eaters é iniciado (botão <Run> para execução contínua ou <Step> para execução passo-a-passo). Neste instante, são repassadas a posição atual ao agente no ambiente e as informações do campo de visão. A Listagem 3 mostra os WMEs da memória de trabalho durante o primeiro ciclo de decisão.

--- input phase ---
--- propose phase ---
Firing propose*move-to-food
Firing propose*move-to-food
Firing propose*move-to-food
--- decision phase ---
     1: O: O1 (move-to-food)
print I1
(I1 ^input-link I2 ^output-link I3)
print I2
(I2 ^eater E2 ^my-location M1 ^random 0.5005971789360046)
print E2
(E2 ^name red ^score 0 ^x 13 ^y 14)
print M1
(M1 ^content eater ^content-name red ^east E3 ^north N1 ^south S3 ^west W1)
print E3
(E3 ^content bonusfood ^east S6 ^north E4 ^south S8 ^west M1)
print N1
(N1 ^content wall ^east E4 ^north N6 ^south M1 ^west N2)
print S3
(S3 ^content normalfood ^east S8 ^north M1 ^south S10 ^west S4)
print W1
(W1 ^content normalfood ^east M1 ^north N2 ^south S4 ^west W2)

Listagem 3 - Estado inicial do programa move-to-food.soar.

Fase de Proposição de Operadores

As regras do programa move-to-food.soar estão fundamentadas em operadores que decidem a direção do agente de acordo com informações sensoriadas do ambiente. O operador move-to-food é o operador responsável por esta indicação de direção. As três regras contidas no programa move-to-food.soar definem basicamente a proposição e a aplicação deste operador assim como a remoção das WMEs associadas a ele para o próximo ciclo.

Ao serem inseridas descrições na estrutura do atributo input-link da memória de trabalho por parte do Eaters, as condições da regra propose*move-to-food são satisfeitas e as ações desta regra são disparadas. Os disparos propõe novos objetos que são instâncias do operador move-to-food para cada célula de comida encontrada no campo de visão. A Listagem 4 mostra o fase de proposição de operadores do ciclo de decisão.

--- propose phase ---
Firing propose*move-to-food
Firing propose*move-to-food
Firing propose*move-to-food

Listagem 4 - Fase de proposição de operadores do programa move-to-food.soar.

Note que três instâncias do operador move-to-food foram criadas e propostas na fase de proposição de operadores mostrado na Listagem 4. Esses três objetos são resultados do disparo da regra propose*move-to-food para as três células de comida encontradas no campo de visão da Figura 6 (direções norte, sul e oeste da posição atual do agente).

Conforme a regra propose*move-to-food especifica, o operador move-to-food é proposto com as preferências: aceitável e indiferente. Estas preferência tem implicação direta na fase seguinte, a fase de decisão.

Fase de Decisão

Uma vez que os operadores são propostos com as preferências aceitável e indiferente, a decisão é feita de acordo com uma escolha aleatória dos operadores propostos. A direção indicada no operador através do atributo ^direction é que representará a direção para o qual o agente deverá mover-se. A Listagem 5 mostra a saída do processamento da fase de decisão no Soar Debugger.

--- decision phase ---
     2: O: O3 (move-to-food)
...
print O3
(O3 ^direction west ^name move-to-food)

Listagem 5 - Fase de decisão do programa move-to-food.soar.

O operador O3 é selecionado. Trata-se de um objeto do tipo operador move-to-food com atributo ^direction definido como west (oeste). A seleção deste operador implica na fase seguinte, a fase de aplicação do operador selecionado.

Fase de Aplicação

Nesta fase, o operador selecionado é aplicado devido ao fato das condições da regra apply*move-to-food serem satisfeitas no estado atual. Os novos WMEs que serão criados representarão que direção o agente deverá mover-se. A Listagem 6 mostra o processamento da fase de aplicação no Soar Debugger.

--- apply phase ---
--- Firing Productions (PE) For State At Depth 1 ---
Firing apply*move-to-food
--- Change Working Memory (PE) ---
print S1
(S1 ^epmem E1 ^io I1 ^operator O3 ^operator O3 + ^operator O1 + ^operator O2 +
       ^reward-link R1 ^smem S2 ^superstate nil ^type state)
print I1
(I1 ^input-link I2 ^output-link I3)
print I3
(I3 ^move M2)
print M2
(M2 ^direction west)

Listagem 5 - Fase de aplicação do programa move-to-food.soar.

Na Listagem 5 é possível notar que um aumento do objeto output-link é criado para indicar ao programa Eaters que esta é a nova direção do agente.

A partir daí, o programa Eaters é notificado da existência do atributo de direção e executa a operação movendo a criatura para a direção indicada.

Persistência de WMEs no programa move-to-food.soar e a consistência da memória de trabalho

A regra de aplicação de operador apply*move-to-food é uma regra do tipo operator-support, ou o-support. Isso porque ela verifica operadores propostos e faz modificações no estado caso seja satisfeita. Regras deste tipo criam novos WMEs que são persistidos na memória de trabalho e válidos para os ciclos de decisão subsequentes. Este é um aspecto interessante da arquitetura porque garante a memorização de eventos anteriores.

A sistemática de persistência traz implicações para o comportamento do programa ao longo da sua execução. Uma vez que WMEs são criados a partir de descrições do objeto output-link, eles são persistidos na memória de trabalho e por lá permanecem até sejam explicitamente removidos.

Cada instância de operador é selecionado e aplicado uma única vez e pode criar novos WMEs que permanecem na memória de trabalho indefinidamente. No caso de programa move-to-food.soar, várias instâncias do operador move-to-food são criadas na memória de trabalho para cada movimento do agente no jogo. Este comportamento pode levar a um esgotamento da memória de trabalho devido ao excessivo número de objetos persistentes.

Para evitar o inconveniente do esgotamento da memória de trabalho, são necessárias regras específicas para a remoção de WMEs que não são mais necessários. Este é o propósito da regra apply*move-to-food*remove-move.

A Regra apply*move-to-food*remove-move

A regra apply*move-to-food*remove-move representa uma sinalização do sistema de saída do Eaters indicando que o movimento recomendado no output-link foi executado com sucesso. A Listagem 6 mostra a regra apply*move-to-food*remove-move.

sp {apply*move-to-food*remove-move
   (state <s> ^io.output-link <ol>
              ^operator.name move-to-food)
   (<ol> ^move <move>)
   (<move> ^status complete)
-->
   (<ol> ^move <move> -)}

Listagem 6 - Código-fonte da regra apply*move-to-food*remove-move.

Quando o atributo ^status é colocado como um aumento do output-link com o valor complete, automaticamente esta regra é satisfeita (matching). Ao ser satisfeita, ela dispara uma ação de remoção do atributo ^move, que é o atributo que indicava a direção de movimentação do agente naquele ciclo.

Ao remover o atributo ^move (a remoção é feita pela preferência de rejeição do WME cujo atributo é ^move no aumento do objeto output-link) as regras de proposição de operadores disparadas anteriormente são retratadas e os WMEs associados à estas regras são removidos da memória de trabalho (tais WMEs estavam persistidos na memória de trabalho). Esta remoção mantém a memória de trabalho consistente e evita seu esgotamento.

Ao serem colocados novas informações de posição do agente e campo de visão, as regras de proposição de operadores são novamente disparadas gerando novos objetos do tipo operador move-to-food. Os acúmulos de WMEs criados pela aplicação deste operador não ocorre mais em função do disparo da regra apply*move-to-food*remove-move assim que o movimento é completado pelo Eaters.

A ausência desta regra provoca um efeito indesejado na memória de trabalho. Este efeito é o acúmulo de objetos do tipo operador move-to-food cuja consequência pode levar, como dito anteriormente, ao esgotamento da memória de trabalho. Este comportamento é explicado pela persistência de WMEs do tipo o-supported na memória de trabalho.

3.1.3. Resultados

Neste experimento foi possível explorar em minúcias o código-fonte e funcionamento de um agente para o jogo Eaters. Foram realizadas várias simulações para testar variações das produções. O agente interpreta as informações capturadas pelos sensores e deliberadamente decide que direção deve ir conforme a presença de comida ao seu redor.

Através da análise do código-fonte do exemplo foi possível:

  • Entender como funciona a interação entre um programa externo e o módulo Soar (enclave);
  • Entender como podem ser representadas as informações de sensoriamento do ambiente;
  • Entender como construir operadores para tratar as informações de entrada e saída;
  • Entender como são tomadas as decisões de movimentação do agente;
  • Entender como construir regras para manutenção da memória de trabalho removendo elementos obsoletos da memória de trabalho, evitando assim o seu esgotamento;

As simulações realizadas no jogo permitiram ainda:

  • Entender a dinâmica dos ciclos de decisão durante a operação do jogo;
  • Inspecionar os estados da memória de trabalho durante a execução do jogo;
  • Simular comportamentos baseados no refinamento das regras;
  • Evidenciar as limitações existentes no agente diante de algumas situações.

A atuação do agente baseado no controle feito pelo programa move-to-food.soar tem sérias limitações. Ele não é capaz de memorizar células que continham comida e não foram visitadas devido à necessidade de escolha por uma em específico.

Outra limitação evidente é a paralisação da criatura quando não há comida nas células imediatamente ao seu redor (fora do campo de visão nas direções norte, sul, leste e oeste).

Algumas soluções, como exercício de especulação, para o problema da paralisia é fazer com que o agente fique procurando sistematicamente independentemente do conteúdo da célula no campo de visão, de forma aleatória.

O mecanismo de preferência por operadores pode, também, inspirar a criação de ações baseadas no conteúdo das células adjacentes. A depender do conteúdo da célula, podem ser atribuídas preferências de movimentação àquelas que contenham bônus no lugar de comida normal e evitar deslocamentos para células vazias ou com outro agente presente.

De forma mais sofisticada, idealmente, o agente poderia criar planos armazenando informações sobre locais visitados e agir no intuito de atingir o propósito previsto pelo plano.


3.2.  Atividade 2: Consolidação de conhecimentos sobre o Soar

3.2.1 Objetivos específicos

  • Usar as ferramentas disponíveis para desenvolvimento e depuração de programas Soar para o jogo Eaters;
  • Realizar as atividades descritas nas seções de 2 a 5 do Tutorial do Soar no intuito de consolidar os conhecimentos obtidos com os atividades anteriores;

3.2.2. Desenvolvimento

Os experimentos desta atividade seguem a sequência de aprendizado estabelecida no Tutorial do Soar. Neste tutorial, as seções fornecem uma abordagem construtiva baseada na proposição de refinamentos do comportamento do agente e cada experimento explora de alguma forma as estruturas e processos básicos do Soar. Muitos deles revisitados para sedimentar os conceitos explorados.

Construindo um agente simples usando regras (move-north.soar)

No caso do Eaters, o estado inicial não é elaborado a partir de operadores, como no caso do Water Jug, mas sim através das informações percebidas do ambiente, mais especificamente, através de modificações realizadas na subestrutura de entrada, o input-link.

Inicialmente, é proposta a criação de um operador para fazer o agente mover-se sempre em direção ao norte. A movimentação do agente no ambiente é realizada através de um aumento da subestrutura de saída, o output-link. O valor indicado pelo atributo direction representa a movimentação do agente nas direções norte, sul, leste e oeste.

Figura 7 - Estrutura para envio de comandos ao agente no jogo Eaters.
Figura 7 - Estrutura para envio de comandos ao agente no jogo Eaters.

As regras que propõem e aplicam o operador para movimentação no sentido norte é mostrado na Listagem 7.

# Propose*move-north:
# If I exist, then propose the move-north operator.

sp {propose*move-north
   (state <s> ^type state)
-->
   (<s> ^operator <o> +)
   (<o> ^name move-north)}

# Apply*move-north:
# If the move-north operator is selected, then generate an output command to
# move north.

sp {apply*move-north
   (state <s> ^operator <o>
              ^io <io>)
   (<io> ^output-link <ol>)
   (<o> ^name move-north)
-->
   (<ol> ^move <move>)
   (<move> ^direction north)}

Listagem 7 - Código-fonte do programa move-north.soar.

A execução deste programa leva o agente a mover-se uma célula em direção ao norte. Depois deste passo, o agente para e não realiza qualquer outro movimento. Este comportamento pode ser visto na Figura 8.

Figura 8 - Comportamento do agente no Eaters: um passo na direção norte
Figura 8 - Comportamento do agente no Eaters: um passo em direção ao norte.

O resultado da execução deste programa pode ser visto na Figura 9.

Figura 9 - Resultado da execução do programa move-north.soar no Soar Debugger
Figura 9 - Resultado da execução do programa move-north.soar no Soar Debugger.

Note que foram criados novos subestados sucessivos depois da conclusão do primeiro ciclo de decisão. Este comportamento se deve ao fato de não haver regras suficientes para propor novos operadores. O estado permaneceu o mesmo desde a última alteração no primeiro ciclo de decisão. É necessário compor novas regras capaz de fazer com que o agente passe a mover-se continuamente.

Múltiplos movimentos (move-north-2.soar)

O comportamento do agente no programa move-north.soar pode ser explicado pelo mecanismo de persistência de Soar.

Durante a fase de aplicação de operador, o operador move-north criou o elemento (O1 ^name move-north) na memória de trabalho. A regra apply*move-north é uma regra de aplicação de operador e, portanto, faz com que o WME criado seja persistido na memória de trabalho (o-supported) logo após a conclusão do primeiro ciclo de decisão.

O fato deste operador persistir na memória permanentemente evita que a regra propose*move-north seja disparada porque ela é sempre satisfeita no estado atual da memória de trabalho. Isso faz com que nenhuma nova instância do operador seja criado e, portanto, leva ao comportamento de parada do agente.

Para tratar adequadamente esta questão, o operador move-north precisa ser retirado da memória de trabalho. Uma regra específica é escrita para esta finalidade. A Listagem 8 mostra o programa move-north.soar contendo a regra apply*move-north*remove-move.

# Propose*move-north:
# If I am at some location, then propose the move-north operator.

sp {propose*move-north
   (state <s> ^io.input-link.eater <e>)
   (<e> ^x <x> ^y <y>)
-->
   (<s> ^operator <o> +)
   (<o> ^name move-north)
}

# Apply*move-north:
# If the move-north operator is selected, then generate an output command to
# move north.

sp {apply*move-north
   (state <s> ^operator.name move-north
              ^io.output-link <ol>)
-->
   (<ol> ^move.direction north)}

# Apply*move-north*remove-move
# If the move-north successfully performs a move command, then remove
# the command from the output-link

sp {apply*move-north*remove-move
   (state <s> ^operator.name move-north
              ^io.output-link <ol>)
   (<ol> ^move <move>)
   (<move> ^status complete)
-->
   (<ol> ^move <move> -)} [- representa preferência de rejeição]

}

Listagem 8 - Código-fonte do programa move-north.soar com a regra para rejeição do operador move-north.

A regra apply*move-north*remove-move possui um atributo ^status cujo valor complete é informado pelo Eaters, representando que o movimento foi realizado pelo agente no mundo externo. A ação da regra indica uma preferência de rejeição ao operador move-north. Isso significa que o operador indicado será retirado da memória de trabalho. Esta regra é disparada durante a fase de aplicação de operadores, em paralelo com a regra apply*move-north. A arquitetura garante a precedência de execução das regras em função da ordem com que elas são satisfeitas.

A retirada do operador da memória de trabalho faz com que a regra de propose*move-north seja, no próximo ciclo de decisão, satisfeita e uma instância do operador move-north é criada. Este fato faz com que a regra apply*move-north seja novamente disparada na fase de aplicação e a subsestrutura de saída informando ao Eaters sobre a nova posição. Os ciclos seguem, agora, de maneira consecutiva e vários movimentos em direção ao norte são propostos. Esta sequência de ações do agente ser vista na Figura 10.

Figura 10 - Comportamento do agente no Eaters: vários passos em direção ao norte
Figura 10 - Comportamento do agente no Eaters: vários passos em direção ao norte.

O resultado da execução deste programa pode ser visto na Figura 11.

Figura 11 - Resultado da execução do programa move-north-2.soar no Soar Debugger
Figura 11 - Resultado da execução do programa move-north-2.soar no Soar Debugger.

As regras propostas ainda não permitem que o agente exiba um comportamento considerado adequado para o jogo. Ele deve mover-se de forma a comer toda a comida disponível. É neste instante que o jogo termina.

Movimentação em direção à comida (move-north.soar)

O agente pode beneficiar-se das informações percebidas no seu campo de visão e atuar de maneira a detectar a presença de comida nas células adjacentes à sua posição. A tarefa passa a ser então criar novas regras que indiquem ao agente a direção a ser seguida baseado na informação de presença ou ausência de comida.

A abordagem ideal para contemplar as demais direções é a generalização das regras criadas até aqui. Elas irão propor e aplicar operadores para as direções adicionais sul, leste e oeste. A proposta de mais de um operador pode levar o Soar à uma condição de impasse. Esta situação indica que o Soar não tem conhecimento suficiente para escolha de um operador em particular para aplicação.

A proposição de operadores no Soar tem uma particularidade: a exigência por preferências. As preferências são especificações de como a escolha por um operador deve ser feita pelo Soar. Para que o Soar considere a escolha, o operador deve ser proposto com a preferência aceitável (+). Da mesma forma, um operador pode ser proposto com a preferência indiferente, representando que este operador não se apresenta como melhor ou pior opção de escolha. O Soar escolhe aleatoriamente tais operadores para aplicação.

A Listagem 9 mostra o programa move-to-food.soar contendo a generalização das regras de proposição e aplicação para contemplar todas as direções possíveis. Além disso, a regra propõe que movimento seja feito apenas quando houver comida nas células adjacentes. A especificação das regras já leva em conta a notação de atalhos e extensão, que são formas mais curtas para denotar aumentos nas regras.

# Propose*move-to-food*normalfood
# If there is normalfood in an adjacent cell,
#    propose move-to-food in the direction of that cell
#    and indicate that this operator can be selected randomly.

sp {propose*move-to-food
   (state <s> ^io.input-link.my-location.<dir>.content
                 << normalfood bonusfood >>)
-->
   (<s> ^operator <o> + =)
   (<o> ^name move-to-food
        ^direction <dir>)}

# Apply*move-to-food
# If the move-to-food operator for a direction is selected,
#    generate an output command to move in that direction.

sp {apply*move-to-food
   (state <s> ^io.output-link <ol>
              ^operator <o>)
   (<o> ^name move-to-food
        ^direction <dir>)
-->
   (<ol> ^move.direction <dir>)}

# Apply*move-to-food*remove-move:
# If the move-to-food operator is selected,
#    and there is a completed move command on the output link,
#    then remove that command.

sp {apply*move-to-food*remove-move
   (state <s> ^io.output-link <ol>
              ^operator.name move-to-food)
   (<ol> ^move <move>)
   (<move> ^status complete)
-->
   (<ol> ^move <move> -)}

Listagem 9 - Código-fonte do programa move-to-food.soar com a generalização.

A Figura 12 mostra o comportamento do agente que, agora, se movimenta em todas as direções em busca de comida.

Figura 12 - Comportamento do agente no Eaters: vários passos nas direções norte, sul, leste e oeste
Figura 12 - Comportamento do agente no Eaters: vários passos nas direções norte, sul, leste e oeste.

O resultado da execução deste programa pode ser visto na Figura 13.

Figura 13 - Resultado da execução do programa move-to-food.soar no Soar Debugger.
Figura 13 - Resultado da execução do programa move-to-food.soar no Soar Debugger.

O agente, agora, passa a mover-se em direção ao norte, sul, leste e oeste. Porém, ainda com um inconveniente: o agente para quando não há comida nas células adjacentes à sua posição atual. Esta situação ocorre porque a regra propose*move-to-food apenas vai propor o operador move-to-food caso exista uma célula de comida nas células adjacentes (normalfood ou bonusfood), como pode ser visto na Listagem 9.

Comandos de Depuração em tempo de execução de programas Soar

O Soar Debugger oferece uma série de comandos que podem ser utilizados em tempo de execução para depuração de programas. Tais comandos obtém informações da memória de trabalho durante as fases do ciclo de decisão e permitem a visualização das estruturas internas do programa, tais como: print, wmes, matches e preferences. A Figura 14  mostra a tela do Soar Debugger com a saída de alguns destes comandos.

Figura 14 - Execução de comandos de depuração em tempo de execução no Soar Debugger.
Figura 14 - Execução de comandos de depuração em tempo de execução no Soar Debugger.

Depuração de erros de sintaxe e semântica nos programas Soar

A depuração pode ser visto como um processo de encontrar defeitos em um programa. No caso do Soar, há dois tipos de erros a serem considerados durante o processo de criação de regras: (i) erros de sintaxe e (ii) erros de semântica.

O Visual Soar é uma ferramenta para desenvolvimento de programas Soar. Ele prove um mecanismo de verificação de erros de sintaxe através da opção Check All Productions for Syntax Errors do menu Datamap. A Figura 15 mostra a tela do Visual Soar indicando a presença de erro de sintaxe propositalmente inserida no programa move-to-food.soar.

Figura 15 - Tela do VisualSoar indicando erro de sintaxe
Figura 15 - Tela do VisualSoar indicando erro de sintaxe.

Os erros de semântica são mais difíceis de serem depurados em função da sua natureza. Eles geralmente não são capturados em uma análise estática do programa sendo descobertos apenas em tempo de execução. O programa semantic-errors.soar contém alguns erros de semântica na construção de regras. A Listagem 10 mostra um erro de semântica durante a criação da regra. Este erro levará a um atributo incorreto na instância do operador e este deixará de ser retirado pela regra que indica a sua rejeição.

sp {propose*move-to-food
   (state <s> ^io.input-link.my-location.<dir>.contant
                 << normalfood bonusfood >>)
-->
   (<s> ^operator <o> + =)
   (<o> ^name move-to-food
        ^direction <dir>)}

sp {apply*move-to-food
   (state <s> ^io.output-link <ol>
              ^operator <o>)
   (<o> ^name move-to-food
        ^direction <dir>)
-->
   (<ol> ^moves.direction <dir>)}

sp {apply*move-to-food*remove-move
   (state <s> ^operator.name move-to-food
              ^io.output-link <ol>)
   (<ol> ^move <move>)
   (<move> ^status complete)
-->
   (<ol> ^move <move> -)}

Listagem 10 - Código-fonte do programa move-to-food.soar com a generalização.

A inserção de defeitos em programas é uma característica inerente do processo de desenvolvimento. É importante conhecer os mecanismos e recursos que as ferramentas de desenvolvimento e depuração oferecem a fim de tornar a descoberta de tais erros um processo mais fácil e rápido.

3.2.3. Resultados

Nesta atividade foi possível construir passo-a-passo um agente do jogo Eaters capaz de mover-se à procura de comida. Ele ainda tem limitações quanto ao seu comportamento mediante algumas situações. Essas limitações serão estudadas nos próximos experimentos. Da mesma forma, foi possível investigar internamente as estruturas e processos envolvidos nos ciclos de decisão durante a execução do agente.

Através do estudo das seções proposta do tutorial foi possível:

  • Reforçar o entendimento sobre a interface de entrada e saída do Soar;
  • Reforçar o entendimento sobre o mecanismo de persistência;
  • Reforçar o entendimento sobre as preferências de operadores aceitável e indiferente;
  • Entender como e quando fazer uso de atalhos e extensões durante a criação de produções;
  • Entender como deve ser feita a depuração de programas à procura de erros;

Os experimentos realizados nesta atividade permitiram que vários conhecimentos vistos na aula anterior pudessem ser melhor sedimentados através de um processo de refinamento construtivo do comportamento de um agente no jogo Eaters.



3.3.  Atividade 3 - Resolução do problema da ausência de comida com saltos

3.3.1. Objetivos específicos

  • Desenvolver um programa em que a criatura não fique presa em posições que não contenham comida e evite comportamentos de ida e volta caso tenham se movido para uma determinada direção sem comida (seções 6 a 8 do Tutorial).

3.3.2. Desenvolvimento

Nesta atividade serão construídas e aprimoradas as regras do programa move-to-food.soar de forma a evitar comportamentos indesejados do agente no jogo Eaters quando não há comida nas proximidades e possibilitar que o agente pule paredes em determinadas situações.

Generalização da movimentação

O programa move-to-food.soar não trata adequadamente o problema da ausência de comida nas células adjacentes à posição atual do agente. É observada uma paralisia do agente quando essa situação ocorre (Figura 12).

Para resolver este problema, pode ser escrita uma regra para proposição de um operador de movimento que leve em conta as células vazias tomando o cuidado de não propor a movimentação para uma célula que contenha uma parede. As regras de aplicação do operador devem levar em conta a indicação de qual direção deve ser o próximo passo na interface de saída e garantir a retirada do operador persistente para que o agente possa mover-se continuamente. Por fim, e bastante relevante, devem ser criadas regras para seleção de operadores propostos a partir de heurísticas representadas na forma de preferências. A Listagem 12 mostra implementação dessas regras.

Movimentação avançada

Um outro comportamento indesejado decorre da escolha aleatória da direção quando há várias células vazias na vizinhança da posição atual do agente: o movimento de ida-e-volta frenética. Este caso em particular faz com que o agente fique indo e voltando em um par de células vazias quaisquer.

Para resolver este problema, podem ser escritas regras que memorizem a última célula visitada e evite que o operador de movimentação em direção à esta célula seja proposto. Desta forma, evita-se a volta a célula de onde o agente saiu. As regras de proposição de movimento devem propor operadores que excluam movimentos que sejam iguais ao oposto do movimento anterior, de forma a evitarem o movimento no sentido oposto de onde o agente veio. As regras de aplicação devem manter criar elementos na memória de trabalho que indiquem o último movimento realizado. E, então, as regras de seleção devem rejeitar qualquer operador que represente um movimento de retorno à célula.

Saltos

Adicionalmente, o agente pode pular paredes à procura de comida. Esta operação tem um custo e diminui a pontuação do agente sempre que executada. Ainda sim, a depender da estratégia definida pelo agente, vale cogitar o salto em detrimento da pontuação.

Para implementar os saltos, podem ser criadas regras que procurem por comida nas células a dois passos de distância da posição atual do agente em qualquer direção.

Solução

Código-fonte: hungry-eater.soar

sp {initialize*state*directions
   (state <ss> ^type state)
   -->
   (<ss> ^directions <n> <e> <s> <w>)
   (<n> ^value north ^opposite south)
   (<e> ^value east  ^opposite west)
   (<s> ^value south ^opposite north)
   (<w> ^value west  ^opposite east)}

sp {init*elaborate*name-content-value
   (state <s> ^type state)
   -->
   (<s> ^name-content-value <c1> <c2> <c3> <c4> <c5>
                       <c6> <c7> <c8>)
   (<c1> ^name move ^content empty ^value 0)
   (<c2> ^name move ^content eater ^value 0)
   (<c3> ^name move ^content normalfood ^value 5)
   (<c4> ^name move ^content bonusfood ^value 10)
   (<c5> ^name jump ^content empty ^value -5)
   (<c6> ^name jump ^content eater ^value -5)
   (<c7> ^name jump ^content normalfood ^value 0)
   (<c8> ^name jump ^content bonusfood ^value 5)}

sp {propose*move*no-backward
   (state <s> ^io.input-link.my-location.<dir>.content { <co> <> wall }
              ^directions <d>
             -^last-direction <o-dir>)
   (<d> ^value <dir>
        ^opposite <o-dir>)
-->
   (<s> ^operator <o> +, =)
   (<o> ^name move
        ^direction <dir>
        ^content <co>)}

sp {propose*jump*no-backward
   (state <s> ^io.input-link.my-location.<dir>.<dir>.content { <co> <> wall  }
              ^directions <d>
             -^last-direction <o-dir>)
   (<d> ^value <dir>
        ^opposite <o-dir>)
-->
   (<s> ^operator <o> +, =)
   (<o> ^name jump
        ^direction <dir>
        ^content <co>)}

sp {apply*action
   (state <s> ^io.output-link <ol>
              ^operator <o>)
   (<o> ^name <action>
        ^direction <dir>)
-->
   (write | | <dir>)
   (<ol> ^<action>.direction <dir>)}

sp {apply*action*remove-move
   (state <s> ^io.output-link <ol>
              ^operator.name <action>)
   (<ol> ^<action> <direction>)
   (<direction> ^status complete)
-->
   (<ol> ^<action> <direction> -)}

sp {apply*action*create*last-direction
   (state <s> ^operator <o>)
   (<o> ^name <action>
        ^direction <direction>)
-->
   (<s> ^last-direction <direction>)}

sp {apply*action*remove*last-direction
   (state <s> ^operator <o>
              ^last-direction <direction>)
   (<o> ^direction <> <direction>
        ^name <action>)
-->
   (<s> ^last-direction <direction> -)}

sp {select*action*bonusfood-better-than-normalfood-empty
   (state <s> ^operator <o1> +
              ^operator <o2> +)
   (<o1> ^name <action>
         ^content bonusfood)
   (<o2> ^name <action>
         ^content << normalfood empty >>)
-->
   (<s> ^operator <o1> > <o2>)}

sp {select*action*empty*move-better-than-jump
   (state <s> ^operator <o1> +
              ^operator <o2> +)
   (<o1> ^name jump
         ^content empty)
   (<o2> ^name move
         ^content empty)
-->
   (write (crlf) | JUMP | )
   (<s> ^operator <o1> < <o2>)}

sp {select*action*food*move-better-than-jump
   (state <s> ^operator <o1> +
              ^operator <o2> +)
   (<o1> ^name move
         ^content << bonusfood normalfood >> )
   (<o2> ^name jump
         ^content << bonusfood normalfood >>)
-->
   (<s> ^operator <o1> > <o2>)}

sp {select*move*bonusfood*normalfood*best*than*jump
   (state <s> ^operator <o> +)
   (<o> ^name move
        ^content << bonusfood normalfood >>)
-->
   (<s> ^operator <o> >)}

sp {select*action*avoid-empty-eater
   (state <s> ^operator <o1> +)
   (<o1> ^name <action>
         ^content << empty eater >>)
-->
   (<s> ^operator <o1> <)}

sp {select*action*reject*backward
   (state <s> ^operator <o> +
              ^directions <d>
              ^last-direction <dir>)
   (<d> ^value <dir>
        ^opposite <o-dir>)
   (<o> ^name <action>
        ^direction <o-dir>)
-->
   (write | Reject | <o-dir>)
   (<s> ^operator <o> -)}

Listagem 11 - Código-fonte da solução para o problema com saltos.

3.3.3. Resultados

Figura 16 - Fim de jogo para o agente hungry-eater.soar.
Figura 16 - Fim de jogo para o agente hungry-eater.soar.

 


3.4.  Atividade 4 - Desafio da busca sistemática por comida

3.4.1. Objetivos específicos

  • Desenvolver um conjunto de regras que sistematicamente busque por comida quando estiver cercado por células sem comida.

3.4.2. Desenvolvimento

Para o problema da sistemática na movimentação quando não há comida nas células adjacentes à posição atual do agente, pode-se procurar no campo de visão do agente se há comida a dois passos de distância. Ao invés de dar uma passo aleatório adiante para ver se encontra comida no campo de visão, pode-se propor um passo na direção da comida mesmo que célula do próximo passo esteja vazia. Este comportamento garante que, dentro do campo de visão da agente, seja proposto o passo na direção da comida como a melhor opção.  

É possível que não se tenha comida no campo de visão do agente em um determinado instante no jogo, especialmente quando há poucas unidades de comida disponíveis. Nestes casos, o agente move-se adiante, repetindo o último movimento selecionado. Ao atingir a proximidade com uma parede, ele muda a direção e inicia uma nova tentativa de encontrar comida nessa direção.

A heurística proposta torna o agente mais ganancioso já que ele prioriza por passos certeiros em direção à comida sem, no entanto, considerar saltos sobre paredes. Uma vez que saltos diminuem a pontuação, esta opção está automaticamente descartada. Apenas as células vazias ao redor e a um passo de distância da próxima comida são interessantes.

Solução

Código-fonte: greedy-eater.soar

sp {initialize*state*directions
   (state <ss> ^type state)
   -->
   (<ss> ^directions <n> <e> <s> <w>)
   (<n> ^value north ^opposite south)
   (<e> ^value east  ^opposite west)
   (<s> ^value south ^opposite north)
   (<w> ^value west  ^opposite east)}

sp {propose*move*no-backward
   (state <s> ^io.input-link.my-location.<dir>.content { <co> <> wall }
              ^directions <d>
             -^last-direction <o-dir>)
   (<d> ^value <dir>
        ^opposite <o-dir>)
-->
   (<s> ^operator <o> +, =)
   (<o> ^name move
        ^direction <dir>
        ^content <co>)}

sp {propose*move*empty-no-backward
   (state <s> ^io.input-link.my-location.<dir>.<dir>.content 
                         { <co> << bonusfood normalfood >> }
              ^io.input-link.my-location.<dir>.content empty
              ^directions <d>)
-->
   (<s> ^operator <o> +, =)
   (<o> ^name move-empty
        ^direction <dir>
        ^content <co>)}

sp {apply*move
   (state <s> ^io.output-link <ol>
              ^operator <o>)
   (<o> ^name << move move-empty >>
        ^direction <dir>)
-->
   (write | | <dir>)
   (<ol> ^move.direction <dir>)}


sp {apply*move*remove-move
   (state <s> ^io.output-link <ol>
              ^operator.name move)
   (<ol> ^move <direction>)
   (<direction> ^status complete)
-->
   (<ol> ^move <direction> -)}

sp {apply*move*create*last-direction
   (state <s> ^operator <o>)
   (<o> ^name << move move-empty >>
        ^direction <direction>)
-->
   (<s> ^last-direction <direction>)}

sp {apply*move*remove*last-direction
   (state <s> ^operator <o>
              ^last-direction <direction>)
   (<o> ^direction <> <direction>
        ^name << move move-empty >> )
-->
   (<s> ^last-direction <direction> -)}

sp {select*move*bonusfood-better-than-normalfood-empty
   (state <s> ^operator <o1> +
              ^operator <o2> +)
   (<o1> ^name move
         ^content bonusfood)
   (<o2> ^name move
         ^content << normalfood empty >> )
-->
   (<s> ^operator <o1> > <o2>)}

sp {select*move*normalfood-better-than-empty
   (state <s> ^operator <o1> +
              ^operator <o2> +)
   (<o1> ^name move
         ^content normalfood)
   (<o2> ^name << move move-empty >>
         ^content empty )
-->
   (<s> ^operator <o1> > <o2>)}

sp {select*move*empty-better-than-move
   (state <s> ^operator <o1> +
              ^operator <o2> +)
   (<o1> ^name move
         ^content empty)
   (<o2> ^name move-empty
         ^content << normalfood bonusfood >>)
-->
   (<s> ^operator <o1> < <o2>)}

sp {select*move*avoid-empty-eater
   (state <s> ^operator <o1> +)
   (<o1> ^name move
         ^content << empty eater >>)
-->
   (<s> ^operator <o1> <)}

sp {select*move*reject*backward
   (state <s> ^operator <o> +
              ^directions <d>
              ^last-direction <dir>)
   (<d> ^value <dir>
        ^opposite <o-dir>)
   (<o> ^name << move move-empty >>
        ^direction <o-dir>)
-->
   (write | Reject | <o-dir>)
   (<s> ^operator <o> -)}

sp {propose*move-empty-cells-last-direction
   (state <s> ^io.input-link.my-location.east.content << empty >>
              ^io.input-link.my-location.west.content << empty >>
              ^io.input-link.my-location.north.content << empty >>
              ^io.input-link.my-location.south.content << empty >>)
-->
   (<s> ^operator <o> +, =)
   (<o> ^name move-empty-cells-last-direction
        ^content <co>)}

sp {apply*move-empty-cells-last-direction
   (state <s> ^io.output-link <ol>
              ^operator <o>
              ^last-direction <dir>
                        )
   (<o> ^name move-empty-cells-last-direction)
-->
   (<ol> ^move.direction <dir>)}

sp {select*move*move-empty-cells-last-direction*better-than*move-empty
   (state <s> ^operator <o1> +
              ^operator <o2> +)
   (<o1> ^name move-empty)
   (<o2> ^name move-empty-cells-last-direction)
-->
   (<s> ^operator <o1> > <o2>)}

Listagem 12 - Código-fonte da solução para o problema da busca sistemática por comida.

3.4.3. Resultados

Figura 17 - Fim de jogo para o agente greedy-eater.soar.
Figura 17 - Fim de jogo para o agente greedy-eater.soar.

 


4.  Conclusão

Os resultados obtidos nos experimentos de acordo com as atividades propostas viabilizaram a sedimentação dos conceitos estudados até aqui sobre o Soar. A abordagem construtiva a partir de um caso real em que o comportamento da criatura é refinado a cada seção também propiciou um aumento no entendimento de como regras podem ser generalizadas e estendidas no intuito de resolver novos problemas proprostos. Da mesma forma, novos conhecimentos foram adquiridos sobre o processo de depuração de erros e os comandos para depuração em tempo de execução. Estes conhecimentos tornarão o processo de desenvolvimento de novos agentes mais eficiente.

 


5.  Referências Bibliográficas

Laird, John E. (2012). The Soar 9 Tutorial. Universidade de Michigan, Michigan. Disponível em: <http://web.eecs.umich.edu/~soar/downloads/Documentation/SoarTutorial/Soar%20Tutorial%20Part%201.pdf>. Acesso em: 8 mar 2013.

Laird, John E. e Congdon, Clare B. (2012). The Soar User's Manual Version 9.3.2. Universidade de Michigan, Michigan. Disponível em: <http://web.eecs.umich.edu/~soar/downloads/Documentation/SoarManual.pdf>. Acesso em: 10 mar 2013.

SML Quick Start Guide (2012). Getting Started with SML Integration. Disponível em: <http://code.google.com/p/soar/wiki/SMLQuickStartGuide>. Acesso em: 12 mar 2013.

Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer