You are here

Aula 3 - SOAR Tutorial 2 - Eaters

Aula 3 - SOAR Tutorial 2 - Eaters

Atividade 1

Como o estado atual da criatura é considerado nas regras ?

Na primeira versão apresentada do programa, temos o conjunto de regras move-to-food que contém 3 regras básicas que movem a criatura na tela:

- a primeira propõe que a criatura se mova para uma célula que contém comida.
- a segunda aplica/efetiva o movimento da criatura ao gerar uma estrutura no link de saída
- a terceira remove a ação de locomoção depois que o ^status dessa passa para 'complete'

As regras consideram elementos na memória de trabalho que mudam a cada aplicação de operadores, assim novas instâncias de operadores continuarão movendo o Eater no tabuleiro. Um exemplo são os dados de posição x e y, que mudam a cada passo do Eater.

Como a decisão de ação escolhida é aplicada de fato ao jogo ?

Nossas regras de aplicação geram estruturas no link de saída do estado para gerar ações no jogo.
Seguimos os atributos até o link de saída e geramos a estrutura move -> direction ->north, como no exemplo a seguir:


Como esse conjunto de regras escolhe a direção para a qual a criatura deve se mover ?

A primeira regra identifica no campo sensorial da criatura em qual direção há uma célula com conteúdo igual a 'normalfood' ou 'bonusfood', e então sugere o operador 'move', que caso seja escolhido, moverá a criatura para aquela direção.

No decorrer do tutorial, veremos que é possivel atribuir preferencias para operadores, de forma que nosso Eater decida mover-se para células que sejam mais interessantes para seus objetivos. Essas preferencias se basearão no conteúdo da potencial célula de destino, como 'normalfood', 'wall', 'empty' e 'bonusfood'.

Para que serve a regra apply*move-to-food*remove-move ? O que aconteceria se ela não existisse ?

A regra apply*move-to-food*remove-move serve para remover do link de saída a ação de 'move'. Caso ela não existisse, o link de saída acumularia comandos antigos, que não são removidos automaticamente por terem sido persistidos na aplicação de um operador. Esse acumulo de vários comandos se tornarão um problema eventualmente, no mímino, ocupando espaço na memória, por isso precisamos de regras para remove-los.

Quais são as limitações desse programa ? O que seria necessário fazer para que ele não ficasse paralizado, depois de um tempo ?

A principal limitação do programa é que não há regras para movimentar a criatura caso ela fique rodeada de células vazias, o que paraliza o programa depois de um certo tempo. Uma solução ingênua seria movimentar a criatura randomicamente para qualquer um dos lados até que ela volta a ficar rodeada por comida e as regras já existentes no move-to-food voltem a se aplicar.

Atividade 2


Uso da interface de entrada e saída de um programa Soar

No caso do Eaters, conforme ele se movimenta, o link-de-entrada é alimentado com informações sensoriais do mundo em sua volta. Podemos visualizar melhor na seguinte ilustração:

O link-de-entrada I2 tem duas extensões:

^eater: contém as informações sobre a criatura: direção, nome, pontos e posição.
^my-location: não especificado na ilustração, mas contém subestruturas adicionais sobre as células ao redor.

Já o link-de-saída, conforme já comentado anteriormente, é a extensão que utilizamos para gerar ações no mundo, criando, a partir desse link, estruturas que representam essas ações.


Uso de shortcuts em programas Soar

Para simplificar a escrita e leitura de regras, o Soar permite que combinemos condições que estão ligadas por variáveis. Para isso, basta concatenar os atributos, substituindo as variáveis por um ponto. Essa técnica é conhecida como "dot notation" ou "path notation".

Da perspectiva do Soar, a regra reescrita com "dot notation" é exatamente a mesma que a escrita mais verbosa.

Um exemplo de código refatorado:

Antes:

Depois:


Uso do SoarJavaDebugger para acompanhar o processo de escolha e aplicação de operadores, por meio de traces.

Um novo recurso apresentado sobre traces e debug é o posicionamento do "breakpoint" no ciclo de execução do SOAR. Para que ao clicarmos em "step" tanto na janela do Eaters quanto no debugger, a execução pare no ponto especificado por nós, para que possamos analisar os traces gerados.

A figura abaixo ilustra o estado inicial do debugger. O ícone verde representa o ponto atual da execução, e o ícone vermelho é o qual arrastamos para o ponto desejado.

Na próxima figura sugiro que a execução pare logo antes da entrada de dados dos sensores, então na linha de comando a entrada "print -d 2 i2" exibe as estruturas de entrada atual:


Diferença entre ações o-supported e i-supported, e WMEs persistentes

Ações o-supported são aquelas criadas por operadores, toda regra que testa um operador selecionado será uma ação operator-supported e serão persistidas. Essas ações rodam na fase de aplicação junto com regras i-supported.
Por serem persistentes conseguimos vê-las com a ajuda do debugger. Na figura seguinte temos o Eaters rodando sem a regra que remove a ação de movimentação:

Ações i-supported retraem assim que as regras que as criaram não são mais válidas. Exemplos são as regras de proposição, elas não são regras de aplicação por não testarem um operador selecionado, elas testam o estado e elaboram apenas sobre operadores propostos. Essas regras rodam na fase de proposição.

WMEs são elementos na memória de trabalho. Se executarmos o Eaters, criarmos uma criatura a partir do conjunto de regras move-to-food e entrarmos com o comando "wmes" sem agumentos no debugger, teremos a seguinte resposta:

O comando "wmes" nos mostra os elementos persistidos na memória do agente. Se usarmos o mesmo comando para um operador, veremos elementos individuais da memória de trabalho, ao invés de objetos inteiros:


Uso de preferências entre operadores

Um recurso muito interessante e poderoso para modelagem de regras dos agentes é o uso dos simbolos de proferência. Como vemos no desenvolvimento do move-to-food ao longo do tutorial, esses símbolos podem ser usados para indicar a melhor opção entre um conjunto de operadores candidatos dado a situação atual.

São eles:

  • Aceitável (+): simplesmente marca que um valor é candidato para seleção.
  • Rejeite (-): marca que um valor não é candidato para seleção e deve ser rejeitado.
  • Melhor (>), Pior (<): indica que um valor não deve ser selecionado caso o melhor valor seja candidato.
  • O Melhor (>): Marca um valor que deve ser selecionado caso ele não seja rejeitado. Ou se não há nenhum valor melhor que ele.
  • O Pior (<): Marca um valor que deve ser selecionado caso não haja mais alternativas.
  • Indiferente (=): Indica que não importa qual valor seja o escolhido.

Uso de extensões em regras

Sobre a eficiencia de escrita de código, temos a seguinte facilidade: Considere as regras que propõem que a criatura mova-se em direção à uma comida normal e outra que propõe que ele se mova em direção à uma comida bonus. Ao invés de escrevermos duas regras, é possível escrever apenas uma que testa um conjunto de valores alternativos. O que nos parece adequado sendo que a única diferença entre essas duas regras é testar por 'normalfood' e 'bonusfood'. O código fica mais legível e economizamos uma regra, como é ilustrado a seguir:



Uso do VisualSoar para detectar erros em regras
Ao utilizarmos o VisualSoar para o desenvolvimentos de agentes, temos algumas ferramentas que nos ajudam a manter o código funcional, elas nos ajudam a identificar erros sintaticos e semanticos.

Erros sintáticos são aqueles que cometemos quando não aderimos à gramática da linguagem ou digitamos algum nome de variável errado. Quando o VisualSoar identificar um erro desse tipo, ele nos dirá na tela de 'feedback' em qual arquivo e qual linha cometemos o erro.

Erros semânticos são aqueles que encontramos em tempo de execução, são erros de lógica que no geral podem apresentar três resultados: uma regra não dispara quando deveria; uma regra dispara quando não deveria; a ação da regra está incorreta.
A única maneira de encontrar e corrigir erros desse tipo é depurando o código. Ou, se preferir, usar a velha técnica dos 'prints' entre as linhas de código.

Uso de comandos do Debugger em tempo de execução

O debugger nos dá alguns comandos que podemos usar em tempo de execução. Portanto devemos sempre escolher o ponto de parada da execução, arrastando o ícone (que lembra uma ampulheta) vermelho para a posição desejada dentro do ciclo de execução.

Alguns comandos são:

  • print: imprime estruturas da memória de trabalho. Um argumento comumente utilizado com esse comando é o 'depth' que define qual a profundidade da recursão que o debugger vai imprimir de extensões do objeto original.
  • matches: retorna uma lista de todas as regras que estão prontas para disparar, caterorizadas por regras que aplicarão operadores, regras que criarão extensões i-support, e regras que removerão extensões i-supported
  • preferences: imprime as preferências para a escolha de um operador. Para usar o comando, passe um identificador e um atributo, o Soar então vai imprimir as preferências para todos os valores para esse par de identificador e atributo.

Atividade 3

Nas próximas seções do tutorial evoluiremos o programa inicial do Eaters para que alguns comportamentos mais avançados sejam adotados para melhores tomadas de decisão.

Last-direction

O tutorial sugere que criemos uma extensão no nosso estado que guarde a última direção para qual caminhou nosso Eater, de forma a evitar que o mesmo ande aleatoriamente pra frente e pra trás. Chamaremos essa nova extensão de ^last-direction

Para isso criamos uma regra que inicializa uma outra extensão chamada '^directions' no nosso estado que lista as direções contrárias:

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)}

Feito isso não precisamos mudar nossas regras já existentes, mas criar outras duas para manter o valor de ^last-direction, ambas as regras dispararão durante a fase de aplicação:

  • A primeira regra, create*last-direction, cria a extensão ^last-direction assim que o operador 'move' é selecionado, o valor guardado em ^last-direction é exatamente a mesma direção do operador 'move'.
  • A segunda regra, remove*last-direction, remove a extensão ^last-direction quando o operador move é selecionado e a direção dele é diferente daquela guardada em ^last-direction.

Nesse ponto já temos as estruturas necessárias para escrevermos nossa regra que impedirá nosso eater de andar pra trás:

A regra propose*move*no-backward:
Se há 'normalfood', 'bonusfood', 'eater', ou 'empty' em uma célula adjacente,
e não há ^last-direction igual ao lado oposto dessa célula,
proponha o operador 'move' nessa direção.

Jumps

O tutorial sugere também a criação do operador jump. Criar novos operadores não muda a maneira como o Soar trabalha, mas nos dá a chance de trabalhar com o que já aprendemos em um problema levemente diferente.

O operador 'jump' pode fazer nosso Eater pular para uma célula há dois movimentos de distancia, portanto, um pulo custa 5 pontos, o mesmo que se ganha por uma 'normalfood'. O Eater pode pular uma parede, mas não para dentro de uma célula com uma parede.

O mais interessante nessa seção em termos de código, é o uso de extenções diretas para caminhar para células mais longe que as adjacentes e a generalização do nome do operador na regra. Seguem exemplos:

Para acessarmos uma célula dois movimentos distantes de onde estamos, baste concatenar as variáveis a partir da célula corrente:

A aplicação do operador 'jump' é exatamente a mesma do operador 'move', exceto pelo nome. Ao invés de criarmos uma nova regra para cada operador, podemos reusar e generalizar a aplicação original do operador permitindo que a regra confira com um operador chamado 'move' ou 'jump', e na ação, copiando o operador correto para o link-de-saída:

Atividade 4

Utilizando a estrutura top-state descrita na seção 10 do tutorial, tente desenvolver um conjunto de regras Soar que sistematicamente busque por comida quando estiver cercado por células sem comida.
// EM ANDAMENTO
// Para acessar o código até o ponto comentado abaixo clique aqui

Para esse exercício, tentei partir do ponto onde, durante o tutorial, não tínhamos nenhuma regra para a criatura quando essa estivesse rodeada por espaços vazios, ou seja, a versão mais simples do 'move-to-food'. Ainda não consegui encontrar uma forma que garanta que o Eater procure por comida em todo o tabuleiro. Eu tentei algumas estratégias:

  • Manter um histórico das células vazias por onde já tínhamos procurado comida, e baseado nisso, eu propunha os operadores de forma a incentivar que a criatura preferisse não passar por essas células novamente. Porém, quando todas as células em volta estavam vazias e já constavam no histórico, o comportamento voltava a ser aleatório.
  • Na implementação atual, tento manter a direção na qual o Eater corre sobre células vazias e vira sempre que encontra um obstáculo, porém, depois de um tempo ele fica girando em volta do tabuleiro pelas bordas. Tentei criar uma regra pra tentar jogá-lo pro meio do tabuleiro após percorrido metade das células em uma das linhas vazias, mas ele não continua seguindo em frente, por algum motivo que estou tentando rever.
  • Tentei contar os passos que a criatura dava na mesma direção sem dar comida, mas acredito não ter encontrar o lugar certo para incrementar esse 'contador', já que as proposições pareciam entrar em loop e geravam montes de elementos "steps" no estado, mesmo minha regra tentando explicitamente remover quaisquer dados antigos antes de gravar um novo.

Mesmo ainda sem sucesso, tenho algumas observações sobre o uso do debugger durante o desenvolvimento:

  • Algo que eu não tinha notado até perceber que certas regras não estavam carregadas, é que o nome que se dá para a regra na primeira linha a identifica unicamente dentro do SOAR. Como eu tinha copiado e colado outra regra, somente a última com o mesmo nome estava sendo carregada, pois o SOAR, aparentemente, só aceita regras com nomes diferentes.
  • O breakpoint foi muito útil durante a depuração, eu estava em uma situação onde operadores estavam empatando, causando um empasse e a quebra do jogo. Então eu posicionei o breakpoint no ciclo de execução para antes da fase de decisão, o que possibilitou a análise do motivo do empate dos operadores. Com essa mesma solução, foi possível também analisar o estado no qual nenhuma das minhas regras conferiam, isto é, quando o comando preferences não retornava nenhuma resposta quando pausado antes da fase de decisão, e assim foi muito mais fácil corrigir o código. Para a análise do estado, dos empates e detalhes das decisões, utilizei os comandos print, matches e preferences, todos já pré-definidos nos painéis superiores na direita do depurador.

Para ilustrar, seguem alguns retornos dos comandos citados acima:

O comando 'preferences':

O comando 'matches':

O comando 'print -d 2 <s>':


Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer