Por que a programação com planejadores é surpreendente
(hillelwayne.com)- O módulo
plannerdo Picat vai além da programação lógica que encontra atribuições de valores e expressa o problema como uma sequência de mudanças de estado até chegar ao estado objetivo - Ao definir o estado inicial
Start, as regras de transiçãoaction(From, To, Action, Cost)e a condição de términofinal(S),best_plan(Start, Plan)encontra o plano de menor custo até o estado final - No exemplo de caminho em grade, movimentação, limites de borda, desvio de obstáculos e visita a vários objetivos são tratados apenas alterando
actionefinal, e a ordem de visita dos objetivos também pode ser fixa ou livre - O recurso de planejamento pode ser combinado com resolução de restrições, permitindo expressar problemas como encontrar a maior sublista que pode ser dividida em somas iguais removendo elementos no partition problem
- O Picat é uma linguagem de pesquisa, então faltam documentação e mensagens de erro melhores, mas como linguagem-ferramenta para resolver rapidamente certos problemas computacionais, pode oferecer soluções mais concisas do que linguagens gerais
A ideia básica de Picat e da programação com planejadores
- Picat é uma linguagem de pesquisa que busca combinar programação lógica, programação imperativa e resolução de restrições
- Na programação imperativa e funcional comum, escreve-se um algoritmo que produz saídas a partir de entradas; já a programação lógica e a resolução de restrições encontram atribuições de valores que satisfazem relações
- No Picat, identificadores não funcionais que começam com minúscula são atoms como
a,b,c, e identificadores que começam com maiúscula são variáveis - Mesmo em expressões com uma variável ainda não definida
Y, comomember(Y, Arr), o Picat pode encontrar e atribuir valores que tornem a expressão verdadeira- Se
Arr = [a, b, c, a], entãoYpode ser um entrea,bec - Em seguida, ao adicionar uma condição como
X != Y, os valores possíveis ficam mais restritos - Mesmo quando a própria lista ainda é desconhecida, como em
member(a, Z), é possível instanciarZcomo uma lista
- Se
Planejamento encontra mudanças de estado, não atribuições de valor
- Em vez de encontrar valores de variáveis que satisfaçam equações, o planejamento busca uma sequência de mudanças de variáveis que alcance um estado final específico
- Um problema de planejamento no Picat precisa de três elementos
- O estado inicial
Start - Funções
actionque representam transições de estado final(S)para decidir se um estado é final
- O estado inicial
- As funções
actiondo Picat devem todas se chamaractione recebem quatro parâmetros- Estado atual
- Próximo estado
- Nome da ação
- Custo
best_plan(Start, Plan)atribui aPlano plano de menor número de etapas ou menor custo necessário para chegar ao estado final- Se todos os custos forem
1, o custo do plano passa a ser o número total de movimentos - Se qualquer plano servir, independentemente do tamanho, pode-se usar
plan(Start, Plan)
- Se todos os custos forem
Exemplo de busca de caminho em grade
- O problema de exemplo consiste em um marcador sobre uma grade que parte da origem
(0, 0)e deve chegar a uma coordenada alvo- Em cada etapa, ele pode se mover uma casa para cima, baixo, esquerda ou direita
- Não pode sair dos limites da grade
- Ao chegar à coordenada alvo, obtém sucesso
- O estado inicial armazena juntos a posição atual e o objetivo, como em
{Origin, Goal}- Em Picat,
{a, b}é sintaxe de array, mas na prática é usada como uma tupla
- Em Picat,
- A condição de término pode ser expressa com pattern matching, como em
final({Pos, Goal}) => Pos = Goal.- Para escrever a mesma coisa sem pattern matching, seria preciso primeiro decompor o estado em
{Pos, Goal} - Se houver várias condições
final, o plano terá sucesso quando qualquer uma delas for verdadeira
- Para escrever a mesma coisa sem pattern matching, seria preciso primeiro decompor o estado em
- A ação de movimento escolhe uma entre quatro direções
{-1,0},{1,0},{0,-1},{0,1}e verifica se a nova coordenada está no intervalo0..10member({Dx, Dy}, Dir)é usado para encontrar valores de direção possíveismember(Tx, 0..10)emember(Ty, 0..10)são usados para verificar se a coordenada está dentro dos limites- Para um predicado de verificação sem atribuição de valor, também existe
membchk
- O plano resultante é exibido como uma lista de ações de movimento e novas coordenadas, como
{move,{1,0}},{move,{2,0}}- Um script em Raku pode ser usado para visualizar o caminho
- Ao adicionar uma condição como
{Tx, Ty} != {2, 1}, também é possível fazer desvio de obstáculos para evitar coordenadas específicas
Vários objetivos e minimização de custo
- Para visitar vários objetivos,
Goaldeixa de ser uma única coordenada e passa a ser uma fila de objetivos, como[{2, 2}, {3, 4}] - Adiciona-se uma nova
actionque remove o item correspondente da lista de objetivos quando se chega a um objetivo[Head|Tail]divide uma lista entre o primeiro elemento e o restanteGoal = [Pos|Rest]só é verdadeiro quando a posição atualPoscoincide com o primeiro item da lista de objetivos- Se o novo estado for
{Pos, Rest}, o objetivo alcançado é removido
- O critério para saber se todos os objetivos foram visitados passa a ser
final({Pos, Goal}) => Goal = [].- A condição de término deixa de ser se a posição atual coincide com um objetivo específico e passa a ser se a lista de objetivos está vazia
- Visitar os objetivos em uma ordem fixa nem sempre produz o caminho global mais curto
- Para ignorar a ordem dos objetivos e minimizar o caminho total, a ação
marké alterada- Em vez de
Goal = [Pos|Rest], usa-semember(Pos, Goal)para verificar se a posição atual aparece em qualquer lugar da lista de objetivos To = {Pos, delete(Goal, Pos)}remove da lista o objetivo visitado- Nesse modelo, o Picat pode escolher qual objetivo visitar em seguida para minimizar o comprimento total do caminho
- Em vez de
Combinação de planejamento e resolução de restrições
- O recurso de planejamento do Picat é integrado a outras funcionalidades da linguagem, permitindo usar planejamento junto com resolução de restrições
- O partition problem usado como exemplo é um problema NP-completo em que uma lista de números deve ser dividida em dois grupos com a mesma soma
- Esse programa remove elementos da lista de números para encontrar a maior sublista que pode ser particionada em somas iguais
- Remover elementos da lista de entrada é tratado como uma ação de planejamento
final(Numbers)verifica se existe uma solução de partition válida para aquela lista de números- Com as restrições do módulo
cp, cada elemento é representado por uma variável0..1indicando se entra no grupo da esquerda ou da direita - A restrição exige que a soma total seja o dobro da soma de um dos lados
- Na saída de exemplo, após remover
[5,17], a lista restante pode ser dividida em dois grupos que somam1108cada32+99+977=1108122+77+86+59+47+154+141+172+49+62+109+30=1108
- Essa abordagem não para em resolver diretamente uma restrição válida: ela também expressa em forma de planejamento as mudanças necessárias para chegar a um estado com restrições válidas
Limitações e usos adequados do Picat
- Como o Picat é uma linguagem de pesquisa, seu uso em produção não é recomendado
- Ela não tem muitos recursos de conveniência e também carece de boa documentação e mensagens de erro claras
- Quando não existe um plano solucionável, o erro aparece como
*** error(failed,main/0)
- Quando não existe um plano solucionável, o erro aparece como
- O fato de rodar no Windows é visto como uma vantagem em relação a muitas linguagens de pesquisa
- O Picat se parece menos com uma linguagem para escrever código que será mantido ou compartilhado e mais com uma linguagem de toolkit voltada a resolver certos tipos de problemas computacionais
- Alguns problemas que eram difíceis de tratar com linguagens de programação gerais e resolvedores de restrições podem ser resolvidos de forma bastante elegante com Picat
Outras linguagens de planejamento e conceitos relacionados
- O planejamento foi originalmente desenvolvido em robótica e IA, mas hoje aparece com frequência em IA para videogames sob o nome Goal Oriented Action Planning (GOAP)
- Em geral, isso é feito como biblioteca sobre outra linguagem ou implementado com estratégias de busca personalizadas
- Uma explicação sobre GOAP pode ser vista neste material
- PDDL é uma linguagem de descrição de planejamento usada como entrada por planejadores independentes
- Ela cumpre um papel semelhante ao do DIMACS no SAT como formato de descrição
1 comentários
Comentários do Hacker News
Já usei o modo de planejamento do Picat em trabalho real
Fiz um protótipo de um sistema para coordenar a manutenção de conjuntos de vários equipamentos, e o modelo era inserir “o que você quer”, “quais ações são possíveis” e “quais restrições precisam ser respeitadas”, não “como fazer”
Em exemplos pequenos ele criava bem planos ótimos, mas, quando cresceu para uma escala real, desmoronou como esperado; problemas de planejamento acabam sendo EXPTIME, então os limites eram grandes
O Picat tem uma válvula de escape para definir heurísticas, então fiz um random forest de predicados de estado e um classificador Naive Bayes preverem caminhos promissores, mas, mesmo com restrições de quebra de simetria e planejamento hierárquico, dava trabalho demais
Parece que ainda há um inverno da IA no domínio clássico da GOFAI
Talvez não, se o problema de planejamento for reformulado para retornar um “gerador em tempo polinomial que produz planos potencialmente exponencialmente longos”
CPLEX, Xpress, GUROBI e Hexaly me vêm à mente, e o Hexaly é especialmente bom para problemas de escalonamento ou roteamento de veículos
Em geral, você acessa isso por APIs para linguagens muito usadas na indústria; acho que essa abordagem faz muito mais sentido do que linguagens dedicadas de solvers, que são fracas para tarefas gerais
Chamar o GUROBI a partir de Python é muito fácil, e você também pode usar normalmente os recursos gerais do Python
O Mosek é muito mais barato que o GUROBI, mas as APIs de ambos são de nível bem baixo, e o desempenho também não chega ao do GUROBI
Esse solver lida facilmente com uma quantidade absurda de variáveis e restrições, e as heurísticas embutidas também são excelentes
Alguns sistemas dividiam o problema em representações diferentes e acoplavam solvers automáticos especializados
Jahob Analysis System e Cyc me vêm à mente
Do ponto de vista de uso real, o design mais limpo da IA clássica foi o Procedural Reasoning System, e eu gostaria de ver uma versão refeita que compensasse seus pontos fracos com métodos modernos
https://en.wikipedia.org/wiki/Procedural_reasoning_system
Em outras áreas de computação numérica, muitas vezes o estado da arte é open source, então sempre me perguntei por que a área de otimização é diferente
Há um site do hakank, usuário ativo do HN na comunidade de programação por restrições, ou seja, Hakan Kjellerstrand, com muitos materiais e exemplos de Picat: http://www.hakank.org/picat/
Como de costume, eu recomendaria Prolog
É elegante, fácil de entender e mais maduro; se você quer resolução de restrições em domínios finitos, a configuração básica já é suficiente
Além disso, o MiniZinc é uma ótima interface para acessar vários solvers especializados em diferentes finalidades, então, se você não for especialista, é provável que chegue mais perto do resultado desejado
Prolog tem muitas vantagens, mas o “faro mecânico” necessário para obter bom desempenho pode crescer bastante rapidamente
Se você escreveu algo em Picat, vale pensar também em como escreveria a mesma coisa em outras linguagens
Esses problemas de brinquedo também são fáceis em outras linguagens, e, na maioria das linguagens funcionais, Dijkstra ou A* cabem em poucas linhas; no fim das contas, muitas vezes tudo se resume a definir o espaço de estados do algoritmo de busca
Fiquei feliz, mas ao mesmo tempo nem um pouco surpreso, ao ver Predrag como revisor
O Firebase technical screen teria sido muito mais fácil com uma ferramenta dessas, e no fim era só mais um problema de otimização
Dá até vontade de resolver de novo em Picat
Ele também faz coisas interessantes na área de linguagens de programação: https://github.com/obi1kenobi/trustfall
Meu primeiro pensamento foi: “parece um sistema de tipos que você precisa resolver manualmente”
Tentei expressar isso ingenuamente em TypeScript, mas
a,becpodiam ser todos iguais, então nada era resolvidoAo tentar expressar corretamente, cheguei a uma forma que parecia utilizável até certo ponto, mas ainda precisava usar assertions e também não conseguia representar direito o tipo de
YEsse processo, na verdade, mostra o quanto programas no estilo de planejadores são poderosos em termos de simplicidade e concisão
TypeScript não parece poderoso o bastante para expressar esse tipo de restrição
Link do experimento no TS Playground: http://tinyurl.com/3p2pzdtn
É bom ver GOAP sendo mencionado de novo
Era o ingrediente secreto que tornava os inimigos de F.E.A.R. tão interessantes, e o artigo de Jeff Orkin sobre como ele funciona também é fácil e divertido de ler
Uso Prolog e um pouco de CLPFD em trabalho real, e é realmente ótimo
Gostaria que estivesse em todos os lugares; mais exatamente, gostaria de manter um núcleo lógico que enfatizasse pureza e empurrar o comportamento imperativo para as bordas
É uma pena que a indústria pareça presa a ferramentas tão ruins
Parece parecido com Prolog e é interessante
Esse estilo de programação em si não é totalmente novo
Aprendi Prolog na universidade e parece bem parecido, mas Prolog não tinha recurso de planejador
Ainda assim, um planejador é uma forma muito elegante e simples de resolver problemas
A parte sobre videogames no fim do texto me deixou curioso
O recurso de planejador permite resolver problemas com muita facilidade em poucas linhas de código claro, mas como será o desempenho em comparação com um algoritmo escrito de forma imperativa?
O Picat parece bastante eficiente em comparação com linguagens semelhantes [1], mas não encontrei uma comparação com linguagens “padrão”
[1]: https://arxiv.org/abs/1405.2538
Eu também tenho o sonho de simplesmente dizer ao computador qual estado ele precisa alcançar
Não conheço bem a comunidade de planejamento nem os solvers, e só brinquei ingenuamente com ortools, mas já tentei gerar código para ir de um estado a um estado-alvo usando A*
Ele gerava instruções assembly para se mover entre estados e até descobria transições de estado ocultas em chamadas de função para chegar ao objetivo
Para acelerar a busca, também rodei em paralelo com Python multiprocessing, e usei geração dinâmica de vizinhos porque cada thread gerava vizinhos de modo diferente
Na tentativa original, paralelizar A* era difícil, então foi preciso fazer sharding
O sonho do meu experimento é informar ao computador “o que tenho agora” e “o que quero” para que ele encontre sozinho o caminho correto de movimentação
Pessoalmente, vejo programação mais como logística, tipo Factorio ou uma fábrica
Por isso chamo de “sliding puzzle”, porque é um quebra-cabeça em que você precisa mover coisas de um lado para o outro para ver a imagem correta
Repositório no GitHub e anotações: https://github.com/samsquire/sliding-puzzle-codegen-memory
Replit: https://replit.com/@Chronological/SlidingPuzzle3