You are here

Aula 02 - Soar

Conceitos basicos sobre o ambiente  de produção SOAR

Soar is a general cognitive architecture for developing systems that exhibit intelligent behavior.



Primeiros passos: conceitos básicos e práticas: (usando o Soar Java Debugger e o Visual Soar )

Os comentários feitos nestá páginas decorrem das leituras do Tutorial 1 e do Manual do Soar (um amplo o material de apoio pode ser encontrado aqui). Há textos específicos sobre o Soar Java Debugger (Introdução ao Soar Java Debugger) e o Visual Soar (Manual do Visual Soar).

 


Conceitos básicos

A STM (ou Working Memory - WM) contém todas as informações sobre o mundo em que o agente Soar está situado, também é o lugar onde ocorre o processo de raciocínio do agente. Isto é, a STM contém dados capturados pelos sensores, os cálculos que o agente efetua, os operadores correntes e as metas que o agente deve cumprir. Em termos de estruturais, a WM é um grafo onde os nós são estados. Desse modo, todo elemento da STM está conectado direta ou indiretamente a um símbolo de estado.

A WM detém o conhecimento que o agente possui do mundo em que está imerso (informação capturada pela percepção) e outros que infere (processo interno de inferência) ao longo do tempo. A representação de uma, digamos,  unidade de conhecimento é um elemento simbólico, uma terna composta por um identificador, um atributo e um valor:  é chamada de Working Memory Element (WME). O primeiro elemento da terna (o identificador) é um nó do grafo (fornece a condição de existência na WM), estabelece a conectividade do grafo (como uma componente conexa), o segundo (o atributo) e o terceiro (o valor)  elementos podem ser constantes (nós terminais ou não). Múltiplos WMEs que compartilham o mesmo identificador pode ser chamados de um "objeto" e cada um desses WMEs que compartilha aquele identificador é chamado de "augmentation" daquele objeto. Podemos dizer que os WMEs ampliam o conhecimento do agente sobre um objeto (fornecem maior detalhamento de um objeto) na medida em que o agente passa a ter mais descrições (augmentations) do objeto.

Utilizamos a figura abaixo para ilustar alguns dos termos introduzidos no parágrafo anterior.

Na figura acima temos no quadro esquerdo uma configuração do mundo dos blocos e do lado direito um exemplo de como a configuração dos blocos é representada na  WM. O primeiro elemento, S1, é  um identificador, um atributo desse identificador é ^mesa e um valor é t1. Cada WME contém uma pedaço específico de informação. Por exemplo, o nome de b1 A. Como já mencionamos, um conjunto de WMEs pode fornecer caracteríticas e relações sobre um mesmo objeto. Por exemplo, b1 é um bloco e b1 está sobre a mesa. Esses WMEs estão relacionados entre si, pois todos contribuem para a descrição de algo que internamente é identificado como b1. Note-se que b1 é um identificador e o conjunto dos WMEs que compartilham esse identificador é um objeto na WM. Cada WME descreve um atributo (cada atributo tem um valor associado) distinto do objeto, por exemplo, o nome, tipo, relação com outro objeto. Desse modo, é natural que qualquer WME com um mesmo identificador seja parte de um mesmo objeto. Ainda, todo objeto na WM deve estar ligado a um estado, direta ou indiretamente (por meio de outros objetos), caso contrário são automaticamente removidos da WM.

Tecnicamente, os identificadores servem como títulos para organizar as informações, note-se que há dois tipos de nós: identificadores (nós não terminais) e constantes (nós terminais). Logo, s1, b1, b2 e t1 são identificadores e os outros nós são constantes (A, Amarelo, bloco, nil, etc). As arestas identificam os atributos (observe-se o uso do símbolo ^), que mantém as informações específicas sobre identificadores. Valores podem ser constantes ou outros identificadores. Se utilizarmos outros identificadores, podemos representar o conhecimento do agente, na WM, via árvores e grafos.

O Soar cria automaticamente na WM, para todo agente, a estrutura da figura abaixo: 

Complementando a nomenclatura iniciada na figura do mundo dos blocosS1 é um identificador (um nó não terminal) de estado (como dissemos, são criados automaticamente pelo Soar) e sua sintaxe consiste de uma única letra seguido de um número. Nós terminais (ou constantes, p.ex., o símbolo state) possuem somente arestas incidentes, exceto os nós ligados como ^input-link (I2) e ^output-link (I3), estes nós podem ter subestruturas associadas a eles.  As arestas denotam (ou são chamadas de) atributos, sintaticamente podem ser identificadas pelo prefixo "^". Evidentemente, somente identificadores possuem atributos. No caso da figura acima, S1 possui três atributos: superstate, io e type. O identificador  I1 possui dois atributos: output-linkinput-link.

Ainda em relação à figura acima, existem cinco WMEs: S1 ^superstate nil; S1 ^io I1; S1 ^type state; I1 ^output-link I2 e I1 ^input-link I3. Este é o conteúdo minimal de WM. Nessa estrutura minimal temos três WMEs compartilhando o mesmo identificador (S1), logo a coleção desses três WMEs forma um objeto (o objeto estado). Na estrutura minimal acima podemos identificar dois objetos: (S1 ^io I1 ^superstate nil ^type state) e (I1 ^input-link I3 ^output-link I2).

 

Cada WME pertinente à um objeto é chamada de augmentation. Em geral, objetos são escritos como uma lista de augmentations (sintaticamentem, as augmentations são escritas entre parênteses). Por exemplo, (S1 ^type state) é uma augmentation na estrutura minimal acima, pertence à descrição do objeto (S1 ^io I1 ^superstate nil ^type state)

Na figura do mundo dos blocos podemos ver que nem todos os objetos formados na WM se referem (ou identificam) à objetos físicos (objetos que possuem existência física), basta considerar o objeto (s1 ^bloco b1 ^bloco b2 ^mesa t1). O objeto S1 serve apenas para organizar outros objetos, relações e propriedades). Outros objetos identificáveis na figura do mundo dos blocos são:
(b1 ^cor azul ^nome A ^sobre b2 ^tipo bloco)(b2 ^cor amarelo ^nome B ^sobre t1 ^tipo bloco) e (t1 ^cor cinza ^nome Mesa ^tipo mesa).

 

  • Atividades cognitivas no Soar

A figura abaixo mostra a dinâmica do Soar em relação ao seu ciclo de decisão.

No Soar, as atividades cognitivas são interpretadas em termos de procura de um espaço de problema, isto é, o conhecimento do agente é organizado num conjunto de estados e num conjunto de operadores, as instanciações orientam o agente para diferentes estados. Um agente conduz, por iteração de seleção e aplicação de operadores, uma busca pelo seu espaço de problemas.

Na fase de entrada (input), as informações capturadas pela percepção do personagemsão processadas e enviadas para a WM, são anexadas numa estrutura fixa da WM chamada de input-link. A fase seguinte processa a elaboração (elaboration), o agente tem acesso a todo o conteúdo da memória de longo prazo (Long-Term Memory - LTM), o agente utiliza todo seu conhecimento para propor estados, sugerir novos operadores e criar preferências entre os operadores. A fase de decisão (selection) ocorre logo após a fase de elaboração, o operador seleção escolhe um operador do conjunto de operadores propostos, a escolha é baseada em preferências entre operadores. Se o conhecimento do personagem é insuficiente para a seleção de um operador, então ocorre um impasse (ou conflito) e um sub-objetivo é criado permitindo ao personagem realizar um raciocínio (passo) intermediário para alcançar sua meta.

Terminada a fase de decisão, se um operador foi selecionado (seleção de um único operador por ciclo de decisão), então aplica-se esse operador, uma ou mais regras são disparadas de forma a modificar os estados (modifica o conteúdo da WM). As modificações atualizam o estado de conhecimento do personagem ou criam comandos numa estrutura fixa na WM chamada output-link. Na fase de saída, ocorre a leitura dos comandos no output-link e suas execuções produzem as ações no ambiente. 

Resumindo, quando as condições de uma regra são aplicadas e casam com o conteúdo da WM, então as produções disparam suas ações que criam(ou removem) um oumais elementos na WM. Essa alteração pode levar o agente a praticar uma determinada ação no ambiente em que está situado, que por sua vez pode provocar mudanças na WM (p.ex., via percepção). O resultado pode ser um novo ciclo.

  • Impasses e Chunks

Todo conhecimento de um agente Soar está representado como regras de produção na LTM, as regras de produção especificam como buscar e utilizar o conhecimento declarativo para resolver problemas; ditam o comportamento do agente. Uma produção (ou regra) é representada na forma C -> A, com C conjunto de condições (símbolos armazenados na memória de trabalho) e A conjunto de ações, regras são utilizadas para selecionar e aplicar operadores. Se as condições de uma regra são satisfeitas pelo conteúdo da WM, então a produção dispara as respectivas ações. Essas ações podem criar ou remover umou mais elementos na WM.

Essas alterações na WM, provocadas por regras, tanto podem desencadear o disparo (ou retração) de regras adicionais (de modo que o ciclo possa se repetir indefinidamente) quanto levar o agente a atingir um estado de quiescência (onde não há mais produções para disparo).

E, como já dissemos, se o conhecimento do agente é insuficiente ocorre um impasse e um sub-objetivo é criado. Este sub-objetivo é criado especificamente para a resolução do impasse criado. Trata-se de um novo estado criado na WM, um nó que é raiz de um grafo que contém WMEs ligados aos estados anteriores ao impasse de modo que o novo estado engloba todas as informações do estado anterior. De fato, o agente Soar ao tentar resolver um impasse pode entrar em outro, e assim, sucessivamente. O limite desse processo é determinado pelo tamanho da pilha de estados. Uma vez resolvido um impasse, todos os sub-objetivos e estados relacionados ao impasse são removidos e o processamento retoma o seu curso.

Um impasse pode ser entendido como aprendizado. Isto é, se o agente não conhece o que fazer a seguir e, após a resolução do impasse, descobre como prosseguir, então o agente aprendeu algo. Um chunking caracteriza essa passagem convertendo o conhecimento aprendido em uma nova regra, armazenada na LTM, que será disparada em todas as ocasiões que o agente atingir o estado que causou o impasse. Após a resolução de um impasse, o Soar cria um conjunto de novas regras (chamadas chunks) cujas condições são os WMEs que foram testados e as ações são aquelas que foram criadas para criar os novos WMEs. Isto é, o Soar mantém um registro das mudanças necessárias para se resolver uma nova ocorrência do impasse.

 


Preparação do material de estudo

  • Instalação do Soar Java Debugger (SJD) e Visual Soar (VS): obtenha o pacote de instalação [ aqui ] - contém ambos o SJD e  o VS. Para a intalação siga as instruções.   Após a instalação do SJD, execute o aplicativo e teremos uma janela como mostra a figura abaixo. 

Na figura acima, destacamos a opção "File" que permite abrir e executar as implementações programas baseados no Soar (arquivos de extensão ".soar"). 

 

  • Uso do Soar Java Debugger: Hello World (exemplos usando regras e operadores)

Iniciamos nosso estudo explorando duas formas de implementação do clássico "Hello  World". A primeria implementação é baseada em regras [ fonte: aqui], a segunda é baseada em operadores [ fonte: aqui ].

Copie os arquivos e abra usando o SJD. A figura abaixo mostra que o programa foi carregado (vide dica na figura acima) e executado usando o botão "Run". A mensagem "Hello World" é exibida ao fim da execução do programa.

 

O Soar é considerado um sistema de produção: um mecanismo que consiste um conjunto de regras, uma memória de trabalho, um matcher, e um procedimento que resolve conflitos entre regras. O esquema geral de uma regra é como segue:

sp {rule*name
    (condition)
    (condition)
    ...
-->
    (action)
    (action)
   ...
}

A sintaxe de uma regra no Soar tem o seguinte formato:  sp { rule name ... --> ...}: após o nome, o identificador da regra, temos o corpo da regra é composto por uma parte IF (elementos que antecedem a seta) e outra THEN (elementos que ocorrem após a seta). A parte  IF determina o conjunto de condições associado a um determinado conjunto de ações (declarada na parte THEN). O uso do elemento sintático "sp" no início de qualquer regra deve-se ao fato de que, no Soar, toda regra começa casando uma estrutura a um estado.

Vejamos um exemplo básico, no caso do Hello World temos uma única regra, abaixo está o esquema da implementação (o código-fonte do programa) do Hello World no Soar:

########################################################################
# This rule writes "Hello World" and halts.
########################################################################

sp {hello-world
   (state <s> ^type state)
-->
   (write |Hello World|)
   (halt)
}

Observe a sintaxe: o nome da regra, a condição e as ações  (write e halt) a serem aplicadas (caso a condição seja satisfeita). O conjunto de ações dispensa maiores comentário, no entanto a sintaxe da condição exige esclarecimento.

No Soar, um estado detém toda informação sobre a situação corrente: isso inclui informações capturadas pela percepção, a descrição de metas correntes e os espaços de problemas existentes (explicamos isso com maiores detalhes no exemplo do problema dos jarros - water jug problem).  Desse modo, é natural que toda regra esteja associada a uma estado, pois a aplicação de uma (ou mais) ação depende de como está situação corrente.

Logo, é necessário que o suposto estado favorável à ação (declarado em ^type state) seja satisfeito para que as ações sejam executadas, isto é, basta que o agente exista. De fato, todo agente, ao ser criado no Soar, possui uma estrutura da forma s1 ^type state  (s1 é sempre o identificador do primeiro estado) na memória de trabalho (Working Memory - WM), essa estrutura declara a existência do agente. Portanto, para que a condição state <s> ^type state seja satisfeita basta que ocorra a instanciação da da variável <s> (que denota o estado corrente) por um identificador, atributo ou valor. No caso do Hello World, o teste de existência do agente verifica se há um identificador, sem testar valores ou atributos específicos. Com a satisfação da condição as ações são executadas. Veremos outras situações mais adiante quando abordarmos o problema dos jarros). 

A seguir analisamos o mesmo problema do Hello World via operadores. Um operador ocasiona um passo (uma ação interna (modifica o estado interno do agente) ou externa (atua no ambiente) em direção à meta corrente)  no espaço de problemas. A escrita do código Soar para o Hello World, via operadores, segue a estrutura do ciclo mostrado numa figura anterior [vide Figura]. 

#######################################################################
# This operator writes "Hello World" and halts.
#######################################################################

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

sp {apply*hello-world
   (state <s> ^operator <o>)
   (<o> ^name hello-world)
-->
   (write |Hello World|)
   (halt)
}

São  duas regras: uma para propor e outra para aplicar o operador. A primeira regra propõe o operador hello-world e a segunda executa as açoes referentes a aplicação do operador. A figura abaixo mostra a execução do código acima.

 

 

 

 

 

 

 

 

 

 

 

 

 

A condição estabelecida para a proposta de um operador é a mesma utilizada no exemplo anterior (utilizando regra). A diferença está na ação, a regra propose*hello-world propõe  o operador de nome hello-world (conforme declarado na parte  THEN<o> ^name hello-world).  A seguinte cadeia de eventos estabelece a aplicação do operador hello-world: o primeiro match estabelece a ligação entre a condição e o operador que executa a ação:

o match (instanciação via match) toma como base o estado: o estado <s> na ação (<s> ^operator <o> +) deve casar com o estado da condição (state <s> ^type state)); e,

uma vez estabelecida a ação, o segundo match identifica o operador que executa a ação:

neste caso, o match toma como base o identificador do operador " <o> "  (o indicador "+" diz que o operador tem preferência aceitável):  apenas um identificado deve casar e, neste caso, é o  <o> ^name hello-world.

Após a escolha do operador pocede-se a sua respectiva aplicação. As condições associadas à aplicação do operador tomam como base a instância do identificador " <o> " que, se satisfeitas, executam as ações (write |Hello World|)  e (halt) .

Esquematicamente, temos a três fases: 

 

 

 

Fazemos agora uma leitura da execução do processo via SJD (vide esquema acima). O disparo de uma regra provoca a criação automática de um novo identificador, que é aplica em todas as ocorrências da variável; neste caso estamos nos referendo à variável  <o> (que aparece na ação, mas não é declarada na condição da proposta). Por exemplo, se O1 é o identificador criado para <o>, então (S1 ^operator O1 +) e (O1 ^name hello-world) são adicionados na WM.

No procedimento de decisão, ocorre a seleção do operador, neste caso a ação do operador propose*hello-world cria apenas um único operador com preferência para aceitação (este operador é identificado por O1). Uma vez feita a escolha, (S1 ^operator O1) é criado na WM, o item que indica a preferência não é mais necessário (o operador já foi escolhido). O valor do identificador também é criado na WM: O1 (hello-world).

Feito a escolha, a regra apply*hello-world  pode ser disparada. Há duas condições de teste para o caso do operador hello-world ser selecionado, a primeira testa se algum operador foi selecionado e a segunda se há um objeto na WM com o nome hello-world. A regra combina as variáveis <o>s e verifica se suas instâncias casam com o mesmo identificador, se for o caso a regra é disparada (e as ações de impressão e parada são executadas).

 

O VS é uma ferramenta própria para a escrita de agentes Soar, a figura abaixo exibe a aparência do VS com o programa water jug simple agent carregado. 

 

Para carregar o agente basta fazer o download do arquivo compactado, descompactar e carregar via opção "File -> Open Projet" do VS o arquivo water-jug.vsa. O programa é carregado na janela de operação e exibe cinco itens. O primeiro item é a raiz da árvore que faz referência ao nome do projeto. Os outros nós são criados automaticamente (explicamos cada um deles no decorrer do texto). O código fonte, como vimos no SJD, contém as regras que determinam o comportamento de um agente Soar. As regras são agrupadas em diferentes arquivos e tipos de arquivos (dependendo da função das regras). O controle de como agrupar as regras é feito a partir desta janela (a janela de operação). Esta forma de estruturar as regras visa dar maior comodidade ao desenvolvedor (para o Soar pouco importa como as regras foram agrupadas). 

O VS possui uma ferramenta chamada Datamap utilizada para descrever a estrutura da WM. O Datamap abre uma janela à direita da janela de operação exibindo a estrutura hierárquica da memória de trabalho do agente.

Para abrir a janela Datamap basta posicionar o cursor do mouse no nome do projeto (neste caso water-jug), pressione o botão direito do mouse para abrir uma janela pop-up e com o cursor do mouse selecione e ative a opção "Open Datamap". o resultado deve ser similar ao da figura abaixo.

O VS permite executar testes para efetuar a depuração de códigos, vejamos como isso funciona com o WJP. O arquivo readme que acompanha o projeto do WJP enuncia o cenário do seguinte modo:

### ABSTRACT. The task is to find the sequence of steps that fill the
### three gallon jug with one gallon of water. There are a  well  that
###
has an infinite amount of water, a five gallon jug,  and  a  three
### gallon jug.

### DESCRIPTION. The task problem  space  has  three operators:  empty,
### fill, and pour. Empty empties a jug into the well. Fill fills up a 
### jug from the well. Pour pours some or all of the contents from one
### jug into the other jug. Pour can only pour out the contents of the
### jug until the source is empty or the destination is full.
### State Structure: Each jug has slots to record its capacity [volume],
### the amount of water it contains [contents], and the capacity availa-
### ble [empty] which is the volume minus the contents.
###   (state s1 ^jug j1)
###   (jug j1 ^volume v1 ^contents c1 ^empty f1),
### where v1, c1, and f1 are numbers.

A figura abaixo ilustra o cenário descrito (a fonte, um jarro com capacidade máxima de três litros e outro de cinco litros). A figura também mostra uma seqüência de passos (da esquerda para a direita):

 

O estado inicial é composto pela fonte de água, e os dois jarros vazios. Da esquerda para a direita, temos a seguinte seqüência de estados obtidos a partir das  operações encher, esvaziar e preencher: encher o jarro de três, preencher o jarro de cinco, encher o jarro de três, preencher o jarro de cinco. Essa seqüência gera o estado final que é o jarro de três contendo um litro de água. Essa seqüência não é necessariamente a única que se espera como parte da resolução do problema, no entanto é a única que atinge o estado final em quatro passos (a partir das operações fixadas), como mostra a figura abaixo. Os estados emoldurados de amarelo são estados-finais e os emoldurados em azul-claro são estados anteriormente obtidos (preferimos repetir os estados e demarcá-los à sobrecarregar o diagrama com setas) - as molduras em azul-claro indicam que o processo de aplicação dos operadores se repete a partir do estado emoldurado. 

 

Vejamos uma implementação de um agente (versão elementar: water-jug simple agent) para a resolução do problema dos jarros. A figura acima ilustra a configuração do espaço do problema: identifica os objetos do problema, o estado inicial, os uso das operações para obter os estados intermediários e quais são os estados finais.

A figura acima revela como são os estados que compõem o espaço do problema, em cada nó (estado) somente o volume de água nos jarros pode ser alterado (de acordo com as operações anteriormente definidas: fill, empty e pour). Nessas condições, podemos estruturar um objeto do seguinte modo: (state <s> ^jug <j1> ^jug <j2>). Note-se que jug é um atributo que pode ter múltiplos valores. Seguindo o esquema da figura, em cada estado, cada jarro é caracterizado pela sua capacidade e conteúdo de água: (jug j1 ^volume v1 ^content c1) e (jug j2 ^volume v2 ^content c2).

Uma vez definida a forma da representação dos elementos que compõem o espaço do problema podemos estabelecer o formato do operador que irá criar o estado inicial. Lembrando que, no  Soar, qualquer ação decorre do ciclo a que estão sujeitos os operadores: elaboração, escolha e aplicação, desse modo, a criação do estado inicial está sujeito à uma condição favorável à ação criadora. Algo como:

  water-jug*propose*initialize-water-jug
  If no task is selected, then propose the initialize-water-jug operator.

O uso do "*" é uma padronização do Soar, serve para separar os nomes, em geral, a primeira parte refere-se ao problema a que está associada, a segunda indica sua função (propose, apply, elaborate) e a terceira indica o nome do operador. A produção acima pode ser codificada no Soar do seguinte modo:

##################################################################################
# Operator that initializes the water jug task initialize-water-jug
# 
#  If no task is selected, then propose the initialize-water-jug operator.
##################################################################################

sp {water-jug*propose*initialize-water-jug
   (state <s> ^superstate nil -^name)      # "-" testa a ausência de um  
   -->                                     # WME com atributo ^name 
   (<s> ^operator <o> +)
   (<o> ^name initialize-water-jug)}

# ------------------------------------------------------------------------
# If the initialize-water-jug operator is selected,
#    then create an empty 5 gallon jug and an empty 3 gallon jug.
# ------------------------------------------------------------------------

sp {water-jug*apply*initialize-water-jug
   (state <s> ^operator.name initialize-water-jug)
   -->
   (<s> ^name water-jug ^jug <j1> <j2>)
   (<j1> ^volume 3 ^contents 0)
   (<j2> ^volume 5 ^contents 0)
   }
  

A primeira regra diz respeito à proposição do operador initialize-water-jug, a condição ^superstate nil verifica a existência de algum desde que não haja nenhum estado com o atributo name (isto é testado em -^name: o símbolo "-" prefixado a um atributo permite testar a ausência de WMEs com o referido atributo). Se satisfeitas as condições, então o operador initialize-water-jug é proposto. A segunda regra diz respeito à aplicação da regra initialize-water-jug. A aplicação adiciona o nome ao estado e cria os jarros de capacidade 3 e 5 colocando em ambos conteúdo 0.

Note que uma regra ao propor um operador cria-o na WM, se satisfeitas as condições para a proposta. E, parece evidente no caso da proposta do initialize-water-jug, que após a criação das estruturas contidas na ação da regra water-jug*apply*initialize-water-jug (isto é, após o disparo do operador initialize-water-jug) o operador initialize-water-jug deva ser removido da WM. O motivo é simples, a condição associada à proposta desse operador jamais será atendida novamente (isso decorre do teste sobre a ausência de WMEs com o atributo name ), portanto não é necessário que esse elemento persista na WM. Evidentemente, uma vez que ocorre a remoção a regra que aplica o operador também perde sua utilidade. Por outro lado, as estruturas criadas na WM pelos operadores removidos devem atender a uma outra política.

No Soar temos dois tipos de persistência na WM. O primeiro refere-se a WMEs que são removidos tão logo eles deixam de ser necessários, como no caso exposto no parágrafo anterior. O segundo está associado aos WMEs que são criados como parte de uma aplicação de um operador, esses WMEs podem persistir indefinidamente, isto é, permanecem na WM até serem removidos explicitamente. Vejamos um exemplo, considere a seguinte regra:

sp {water-jug*elaborate*empty
     (state <s> ^name water-jug
                ^jug <j>)
     (<j> ^volume <v>
          ^contents <c>)
    -->
     (<j> ^empty (- <v> <c>))  # uso de notação pré-fixa para "<v> - <c>"  
   }
  

O operador inicialização criou dois jarros com suas respectivas capacidades e o volume de água inicial (no caso, ambos vazios).  A transição de estados ocorre pela aplicação dos operadores fill, pour e empty que depende do conhecimento sobre o volume de água que um jarro pode comportar num dado estado, isto é, a diferença entre a capacidade do jarro e a quantidade de água contida nele (isto é, de ^empty (- <v> <c>) ). As regras de elaboração de estado são usadas para  derivar novos conhecimentos a partir de WMEs, criam novas descrições (isto é, novas augmentations) do situação corrente e podem servir de apoio para os operadores de seleção e aplicação. As novas descrições servem para, digamos, para tornar mais simples os testes de seleção e aplicação dos operadores. Note-se que essas descrições são temporárias, isto é, os atributos associados a essas descrições (no caso da regra acima o atributo é o ^empty: quando o conteúdo de um jarro é alterado o valor de o ^empty  é removido e dispara o cálculo de um novo valor); os WMEs criados dessa forma são chamados de instantiation-supported (ou i-supported) WMEs (quando a instanciação é retirada o WME é removido, a regra casa os novos valores e produz um novo WME).

Esquematicamente: para cada conjunto de WMEs que casam com a regra uma instância é criada: 

     
     (S1 ^name water-jug)             (S1 ^name water-jug)
     (S1 ^jug j1)                     (S1 ^jug j2)
     (j1 ^volume 5)                   (j2 ^volume 3)   <- WME persistente
     (j1 ^contents 0)                 (j2 ^contents 0) <- WME persistente

As duas instanciações são disparadas e dois novos WMEs são criados.
     
     (j1 ^empty 5)                    (j1 ^empty 5) <- WME não-persistente 
   
  

A partir desses elementos podemos estudar as regras referentes às operações: fill, empty e pour. Para carregar os códigos no VS basta efetuar um duplo clique com o mouse (botão esquerdo) na regra desejada. A figura abaixo exibe a abertura de uma janela com o código-fonte da operação fill.

 Abaixo segue o código da regra que trata a operação  fill:

#######################################################################
# If the task is water-jug and there is a jug that is not full, then 
# propose filling that jug.
#######################################################################     

sp {water-jug*propose*fill
    (state <s> ^name water-jug ^jug <j>)
    (<j> ^empty > 0)   # estabelece a condição do jarro não estar cheio
   -->
    (<s> ^operator <o> + =)         # "=" diz que a preferência é   
    (<o> ^name fill ^fill-jug <j>)  # indiferente (escolha randômica)
   }

# ---------------------------------------------------------------------
# If the task is water-jug and the fill operator is selected for a 
# given jug, then set that jug's contents to be its volume.

sp {water-jug*apply*fill
    (state <s> ^name water-jug ^operator <o> ^jug <j>)
    (<o> ^name fill ^fill-jug <j>)
    (<j> ^volume <volume> ^contents <contents>)
   -->
    (<j> ^contents <volume> <contents> -) # "-" usado para remover um WME.
   }                                      # se fill é selecionado para um 
                                          # dos jarros, então o conteúdo 
                                          # (contents) do jarro muda para 
                                          # <volume>
 

Esquematicamente, vemos múltiplas instâncias: 

sp {water-jug*propose*fill
    (state <s> ^name water-jug ^jug <j>)
    (<j> ^empty > 0)          
   -->
    (<s> ^operator <o> + =)   
    (<o> ^name fill ^fill-jug <j>)
   }

É cria uma instância para cada conjunto de WMEs que casam com a regra::

     (S1 ^jug j1)                     (S1 ^jug j2)
     (j1 ^empty 5)                    (j2 ^empty 3) 
 
As duas instanciações são disparadas criando novos WMEs e preferências: 

WMEs:     
     (S1 ^operator O1 +)              (S1 ^operator O2 +)
     (O1 ^name fill)                  (O2 ^name fill)
     (O1 ^fill-jug j1)                (O2 ^fill-jug j2)

Preferências:  
     (S1 ^operator O1 +)              (S1 ^operator O2 +)
     (S1 ^operator O1 =)              (S1 ^operator O2 =)  

E, apenas um operador será escolhido. 
  

As outras duas regras seguem abaixo:  

#######################################################################
# If the task is water-jug and there is a jug that is not empty,
# then propose emptying that jug.
#######################################################################

sp {water-jug*propose*empty
    (state <s> ^name water-jug ^jug <j>)
    (<j> ^contents > 0)    # verfifica se o jarro contém água
   -->
    (<s> ^operator <o> + =) 
    (<o> ^name empty ^empty-jug <j>)
   }

# --------------------------------------------------------------------
# If the task is water-jug and the empty operator is selected for a 
# given jug, then set that jug's contents to be 0 and its empty to be 
# its volume.

sp {water-jug*apply*empty
    (state <s> ^name water-jug  ^operator <o>  ^jug <j>)
    (<o> ^name empty  ^empty-jug <j>)
    (<j> ^volume <volume>  ^contents <contents>)
   -->
    (<j> ^contents 0  ^contents <contents> -)  # se empty é selecionado 
   }                                           # para um dos jarros, 
                                               # então contents muda 
                                               # para 0.   
 

Antes de apresentarmos a regra que trata do pour, fazemos uma observação em relação ao controle sobre operações. Uma forma de controle que evita a aplicação de uma operação que desfaz o estado obtido, por exemplo: encher um jarro e esvaziar o mesmo jarro em seguida.  Para proceder esse tipo de controle é necessário manter um histórico sobre a última operação.

As outras duas regras seguem abaixo:  

#######################################################################
# If an operator is selected record its name in last-operator 
#######################################################################

sp {water-jug*record*operator 
    (state <s> ^name water-jug ^operator.name <name>) # notação "."  
   -->
    (<s> ^last-operator <name>) 
   }

# --------------------------------------------------------------------
# If an operator is selected and its name is different than last 
# operator, remove last-operator

sp {water-jug*remove*last-operator 
    (state <s> ^name water-jug ^last-operator <name> ^operator <o>)
    (<o> ^name <> <name>) # "<>" sinal de diferente
   -->
    (<s> ^last-operator <name> -) 
   }
 

O código acima mostra duas partes. A primeira, cria uma estrutura na WM referente à operação mais recente. A segunda, remove qualquer registro referente a um operador mais antigo. No caso do WJP temos três operações básicas (fill, pour e empty), desse modo devemos ter regras de controle para cada uma dessas operações. Por exemplo, para o par fill-empty as regras de controle podem ser:  

#######################################################################
# If just applied fill, don't apply empty
#######################################################################

sp {water-jug*select*fill*empty*worst 
    (state <s> ^name water-jug ^last-operator fill ^operator <o> +)  
    (<o> ^name empty)
   -->
    (<s> ^operator <o> < ) 
   }

# --------------------------------------------------------------------
# If just applied empty, don't apply fill

sp {water-jug*select*empty*fill*worst 
    (state <s> ^name water-jug ^last-operator empty ^operator <o> +)
    (<o> ^name fill)
   -->
    (<s> ^operator <o> < ) 
   }
 

As outras regras de controle seguem o mesmo esquema. Observamos que essas regras não visam nenhum tipo de planejamento, apesar de evitarem laços triviais (p.ex., enche-esvazia um mesmo jarro) não evitam que o processo de solução passe por um mesmo estado mais de uma vez. Apresentamos agora a regra que trata do pour:  

#######################################################################
# If the task is water-jug and there is a jug that is not full and the 
# other jug is not empty, then propose pouring water from the second 
# jug into the first jug.

sp {water-jug*propose*pour
    (state <s> ^name water-jug  ^jug <i> { <> <i> <j> })
    (<i> ^contents > 0 )
    (<j> ^empty > 0)
   -->
    (<s> ^operator <o> + =)
    (<o> ^name pour ^empty-jug <i> ^fill-jug <j>)
   }

# -------------------------------------------------------------------
# If the task is water-jug and the pour operator is selected,
# and the contents of the jug being emptied are less than or equal to
# the empty amount of the jug being filled,
# then set the contents of the jug being emptied to 0; set the 
# contents of the jug being filled to the sum of the two jugs.

sp {water-jug*apply*pour*not-empty-source
    (state <s> ^name water-jug  ^operator <o>)
    (<o> ^name pour  ^empty-jug <i>  ^fill-jug <j>)
    (<j> ^volume <jvol>   ^contents <jcon>  ^empty <jempty>)
    (<i> ^volume <ivol>  ^contents { <icon> <= <jempty> })
   -->
    (<i> ^contents 0 <icon> -)
    (<j> ^contents (+ <jcon> <icon>) <jcon> - )
   }

# ------------------------------------------------------------------
# If the task is water-jug and the pour operator is selected, and
# the contents of the jug being emptied are greater than the empty 
# amount of the jug being filled,
# then set the contents of the jug being emptied to its contents 
# minus the empty of the jug being filled; set the contents of the 
# jug filled to its volume.

sp {water-jug*apply*pour*empty-source
    (state <s> ^name water-jug  ^operator <o>)
    (<o> ^name pour  ^empty-jug <i>  ^fill-jug <j>)
    (<i> ^volume <ivol>  ^contents { <icon> > <jempty> })
    (<j> ^volume <jvol>  ^contents <jcon>  ^empty <jempty>)
   -->
    (<i> ^contents (- <icon> <jempty>) <icon> - )
    (<j> ^contents <jvol> <jcon> -)
   }

A busca no espaço de estados pode ser otimizada impondo preferências (melhorando a chance de escolhas) a certos operadores. Isso significa embutir uma heurística na estrutura das regras que formam o conhecimento do agente. Por exemplo, uma forma de não visitar o mesmo estádo várias vezes (talvez, criando uma cópia de cada um dos estados visitados). Não tratamos dessas estratégias neste tutorial, voltaremos a esse assunto nas próximas atividades. Finalmente, a regra que trata da meta (do estado desejado): 

#######################################################################
# If there is a jug with volume three and contents one, then write that 
# the problem has been solved and halt. 

sp { water-jug*detect*goal*achieved
     (state <s> ^name water-jug ^jug <j>)
     (<j> ^volume 3  ^contents 1) 
   --> 
     (write (crlf) |The problem has been solved.|)
     (halt)
   }
 

Obtenha aqui o código-fonte do problema dos jarros escrito no VS. Nas próximas atividades (2, 3, 4 e 5) exploramos o Soar em experimentos mais complexos. Retomamos alguns conceitos como a persistência dos WMEs e exploramos outros como os chunks.   


Considerações

Em certo aspecto, o Soar como exemplo de arquitetura cognitiva pode ser visto como um sistema que se coloca do outro lado dos sistemas especialistas. Sistemas especialistas são centrados em uma particular competência (p. ex., linguagem ou aprendizado) ou  estão comprometidos com alguma forma de comportamento (em certas habilidades) definidas pelo contexto. Em contraste, o Soar (e outros exemplos de arquiteturas cognitivas) procura atuar num escopo mais amplo, buscando obter sistemas que exibam comportamento com diferentes níveis de inteligência, ao invés de sistemas compostos por módulos concebidos para tarefas especificas (módulos especializados). 

Apesar da estrutura do Soar seguir o modelo dos sistemas de produção onde as construções são estruturadas e essencialmente modulares (onde regras podem ser adicionadas, removidas ou modificadas de forma independente), um dos "pontos fracos" está exatamente nesta característica de individualidade das regras: as regras não invocam umas as outras.

 

Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer