Planificación: STRIPS, el mundo de bloques, heurísticas a partir de la relajación y la planificación en los agentes de IA
Los lenguajes STRIPS y PDDL, la planificación hacia delante y hacia atrás, las heurísticas hmax y hadd, la anomalía de Sussman, la planificación jerárquica y la replanificación: del robot Shakey a los agentes de IA que se escriben su propia lista de tareas.
Serie Fundamentos de IA Parte 25 de 52
- Mapa de la inteligencia artificial: qué es cada cosa y cómo encaja todo
- Breve historia de la inteligencia artificial: de Turing a los modelos de lenguaje
- Datos, modelo, entrenamiento, inferencia: los conceptos básicos en un solo ejemplo
- Cómo leer la notación matemática: símbolos, subíndices, sumatorios y cuantificadores
- Conjuntos y relaciones: las piezas con las que se construyen las definiciones en IA
- Funciones y aplicaciones: por qué todo modelo de IA es una función
- Lógica y demostraciones: las reglas con las que se razona en IA
- Grafos y árboles: la estructura sobre la que se apoyan la búsqueda y las redes neuronales
- Álgebra lineal para aprendizaje automático: vectores, matrices y por qué la IA sobre todo multiplica
- Derivadas, gradiente y regla de la cadena: cómo sabe un modelo en qué dirección aprender
- Descenso de gradiente: cómo aprende un modelo
- Qué es la probabilidad: del lanzamiento de un dado a los axiomas de Kolmogórov
- Probabilidad condicionada y teorema de Bayes: cómo cambiar una estimación cuando te enteras de algo nuevo
- Variable aleatoria y función de distribución: una sola función que lo dice todo sobre la distribución
- Función de densidad y función de probabilidad: por qué una variable continua tiene probabilidad cero en cada punto
- Esperanza, varianza y covarianza: dónde está una distribución, qué anchura tiene y qué se relaciona con qué
- Resumen de las distribuciones de probabilidad: Bernoulli, binomial, Poisson, uniforme, normal y exponencial
- Ley de los grandes números y teorema central del límite: por qué las medias se comportan de forma tan predecible
- Estadística y estimación de parámetros: máxima verosimilitud, MAP e intervalos de confianza
- Teoría de la información: entropía, entropía cruzada y divergencia KL, el origen de las funciones de pérdida
- Búsqueda en el espacio de estados: BFS, DFS, UCS y A* con animaciones
- Juegos y adversarios: minimax, poda alfa-beta y Monte Carlo tree search
- Satisfacción de restricciones: backtracking, propagación de restricciones y AC-3 con un sudoku y un mapa de regiones checas
- Lógica y sistemas basados en conocimiento: encadenamiento hacia delante y hacia atrás, resolución y sistemas expertos
- Planificación: STRIPS, el mundo de bloques, heurísticas a partir de la relajación y la planificación en los agentes de IA
- Búsqueda local y recocido simulado: un viajante por las capitales regionales checas
- Algoritmos genéticos: población, cruce, mutación y selección en una mochila para la excursión
- Estrategias evolutivas y programación genética: de la regla de un quinto a la ley de Kepler
- Optimización por colonia de hormigas: cómo un rastro de feromona encuentra un camino corto
- Optimización por enjambre de partículas: inercia, experiencia propia y experiencia del enjambre
- Neuroevolución: evolución de los pesos y la forma de las redes neuronales, y el algoritmo NEAT
- Conjuntos difusos y funciones de pertenencia: cómo escribir «algo cálido»
- Lógica difusa y probabilidad: por qué «algo cálido» no significa «probablemente cálido»
- Lógica difusa: t-normas, implicaciones y reglas SI–ENTONCES
- Control difuso: Mamdani y Sugeno, del ventilador al péndulo invertido sobre un carro
- Agrupamiento difuso: el algoritmo fuzzy c-means y los puntos entre clusters
- Sistemas neuro-difusos (ANFIS): reglas difusas que aprenden de los datos
- Cadenas de Markov: matriz de transición, distribución estacionaria y PageRank
- Modelos ocultos de Márkov: el algoritmo de Viterbi y el casino deshonesto
- Redes bayesianas: por qué está mojada la hierba y qué significa eso para el aspersor
- El filtro de Kalman: cómo estimar a partir de mediciones imprecisas dónde está un coche y a qué velocidad va
- Tipos de aprendizaje automático: supervisado, no supervisado, semisupervisado y más
- Prompts, contexto y RAG: cómo darle a un modelo la información correcta
- Autocodificadores variacionales (VAE): imágenes nuevas a partir de un espacio latente continuo
- Redes generativas adversarias (GAN): el falsificador contra el detective
- Modelos de difusión: cómo el ruido puro se convierte en una imagen
- Procesos de decisión de Markov: estados, acciones, recompensas y ecuaciones de Bellman
- Q-learning y DQN: aprender el valor de las acciones, de la tabla a la red neuronal
- Gradiente de la política y PPO: aprender directamente la política
- Cómo funciona un agente de IA: un modelo de lenguaje en un bucle con herramientas
- Programación con agentes de IA: encargo, pruebas y revisión
- Evaluación y seguridad de los sistemas de IA: cómo medir y cómo vigilar
Temario completo de la serie 52 de 83 temas publicados
- Cómo escribir un problema de planificación como átomos, acciones con precondiciones y efectos, y un objetivo.
- Cómo calcula el planificador por sí mismo una heurística a partir del problema relajado y cuánto trabajo se ahorra con ello.
- Por qué los subobjetivos no pueden cumplirse uno tras otro y cómo planifican los agentes de IA actuales.
Conviene conocer antes: Búsqueda en el espacio de estados: BFS, DFS, UCS y A* con animaciones, Lógica y sistemas basados en conocimiento: encadenamiento hacia delante y hacia atrás, resolución y sistemas expertos
A finales de los años sesenta, el robot Shakey del Instituto de Investigación de Stanford recorría varias habitaciones, empujaba cajas y apagaba luces. Fue el primer robot que razonaba sobre sus actos: recibía un objetivo («lleva la caja a la habitación de al lado») y él mismo deducía qué acciones, y en qué orden, lo conducían hasta él. El lenguaje en el que lo hacía se llama STRIPS, y sus ideas siguen hoy en la logística, en los videojuegos y en el bucle del agente de IA que te escribe código.
Continúa las partes Búsqueda en el espacio de estados y Lógica y sistemas basados en conocimiento.
1. Planificar: una búsqueda que ve por dentro
Planificar es buscar una secuencia de acciones que lleve al objetivo, exactamente como en la parte sobre búsqueda. La diferencia está en cómo se describe el problema. En el laberinto, el estado era una caja negra y el algoritmo solo sabía adónde llevaban las acciones desde él. El planificador ve por dentro: el estado es una lista de hechos sobre el mundo y cada acción dice qué hechos necesita y cuáles cambia. Gracias a ello, el planificador puede deducir por sí mismo una estimación de lo lejos que está el objetivo: no hace falta escribirle ninguna heurística a mano.
2. El lenguaje STRIPS
Definición — problema de planificación STRIPS
Se tiene un conjunto finito de átomos F (proposiciones sobre el mundo). Un estado es un subconjunto s ⊆ F: los átomos que se cumplen en él; lo que no está en s no se cumple.
Una acción a es una terna (pre(a), add(a), del(a)) de subconjuntos de F: las precondiciones y los átomos añadidos y eliminados. La acción es aplicable en s si pre(a) ⊆ s, y el estado resultante es
T(s, a) = (s ∖ del(a)) ∪ add(a)
- T(s, a)
- el estado tras ejecutar la acción: la función de transición que en la parte sobre búsqueda aportaba el programador; aquí el planificador la deduce por sí mismo de la descripción de la acción
- s
- el estado de partida: todo lo que la acción no menciona pasa sin cambios al nuevo estado (el supuesto de STRIPS que resuelve el problema del marco: no hay que enumerar lo que no cambia)
- del(a)
- los átomos que la acción invalida; se restan mediante la diferencia de conjuntos ∖
- add(a)
- los átomos que la acción introduce; se añaden con la unión después de eliminar, de modo que un átomo que está en ambas listas acaba cumpliéndose
Un problema es una cuaterna (F, A, s0, G), donde G ⊆ F es el objetivo. Un plan es una secuencia de acciones a1, …, ak, cada una aplicable en el estado que dejan las anteriores, tal que en el último estado sk se cumple G ⊆ sk.
Un estado es una lista de frases que se cumplen en este momento: «C está sobre A», «sobre B no hay nada». Una acción es una receta con tres casillas: qué debe cumplirse para poder hacerla, qué pasa a cumplirse y qué deja de cumplirse. El objetivo no tiene que describir el mundo entero: basta con decir lo que te importa, y el resto da igual.
Ejemplo — una acción paso a paso
El bloque C está sobre A y B está al lado. La acción a la mesa(C, A) tiene pre = {sobre(C, A), libre(C)}, add = {mesa(C), libre(A)}, del = {sobre(C, A)} (la descripción completa de las acciones está en la sección siguiente).
s = {sobre(C, A), mesa(A), mesa(B), libre(B), libre(C)}
pre ⊆ s? sobre(C, A) ∈ s, libre(C) ∈ s → la acción es aplicable
s ∖ del = {mesa(A), mesa(B), libre(B), libre(C)}
(s ∖ del) ∪ add = {mesa(A), mesa(B), mesa(C), libre(A), libre(B), libre(C)} — los tres bloques sobre la mesa
STRIPS nació en 1971 (Fikes y Nilsson) precisamente para Shakey. Su sucesor actual es el lenguaje PDDL, en el que desde 1998 se plantean los problemas de las competiciones internacionales de planificadores. En él, una acción se escribe como un esquema con variables:
(:action a-mesa
:parameters (?a ?b)
:precondition (and (sobre ?a ?b) (libre ?a))
:effect (and (mesa ?a) (libre ?b) (not (sobre ?a ?b))))
3. El mundo de bloques
El problema de prueba clásico de la planificación: sobre una mesa hay bloques, algunos apilados. Con una acción se toma un bloque que no tiene nada encima y se deja sobre la mesa o sobre otro bloque libre. Hay tres tipos de átomos: sobre(A, B) (A está sobre B), mesa(A) y libre(A).
Acciones en el mundo de bloques
- a la mesa(A, B): pre = {sobre(A, B), libre(A)}, add = {mesa(A), libre(B)}, del = {sobre(A, B)}
- desde la mesa(A, C): pre = {mesa(A), libre(A), libre(C)}, add = {sobre(A, C)}, del = {mesa(A), libre(C)}
- mover(A, B, C): pre = {sobre(A, B), libre(A), libre(C)}, add = {sobre(A, C), libre(B)}, del = {sobre(A, B), libre(C)}
Si bajas A de encima de B a la mesa, A quedará sobre la mesa, B quedará libre y A dejará de estar sobre B. Todo lo demás sigue como estaba, y eso es importante: STRIPS supone que una acción cambia solo lo que ella misma dice.
Antes de nada, el planificador instancia el esquema con variables: sustituye las variables por todas las combinaciones de bloques. Para n bloques hay n(n − 1) acciones «a la mesa», otras tantas «desde la mesa» y n(n − 1)(n − 2) movimientos, en total n2(n − 1). Para tres bloques son 9 · 2 = 18 acciones; para seis, 36 · 5 = 180. Pero en cada estado solo unas pocas son aplicables: hay pocos bloques sin nada encima.
El mundo parece un juguete, pero crece deprisa. Tres bloques pueden colocarse de 13 maneras, cuatro de 73, cinco de 501 y seis ya de 4 051. De dónde salen estos números:
número de estados = ∑k=1n n! · C(n − 1, k − 1) / k!
- n!
- ponemos todos los bloques en una sola fila: cada orden es un candidato, y las torres se obtienen cortándolo
- C(n − 1, k − 1)
- cortamos la fila en k torres no vacías: elegimos k − 1 de los n − 1 huecos entre bloques. El orden dentro de una torre importa (A sobre B no es B sobre A), por eso cortamos un orden y no un conjunto
- k!
- el orden de las torres sobre la mesa no importa: dos torres una al lado de otra son el mismo estado, las enumeremos en el orden que sea. Así que cada estado lo hemos contado k! veces
n = 3: k = 1: 6 · 1 / 1 = 6, k = 2: 6 · 2 / 2 = 6, k = 3: 6 · 1 / 6 = 1 → 13
n = 4: 24 + 36 + 12 + 1 = 73
n = 6: 720 + 1 800 + 1 200 + 300 + 30 + 1 = 4 051
El planificador los contó de otra manera, recorriendo todos los estados alcanzables, y obtuvo lo mismo. Probaremos dos problemas. En el mayor, sobre la mesa hay dos torres A–B–C y D–E–F (escritas de abajo arriba) y el objetivo es una única torre F–E–D–C–B–A:
4. Planificación hacia delante
Definición — planificación hacia delante (progresión)
Búsqueda en el espacio de estados desde el estado s0: los sucesores del estado s son los estados T(s, a) para todas las acciones a aplicables en s. Prueba de objetivo: G ⊆ s.
Desde la situación inicial prueba todos los movimientos, desde cada resultado otra vez todos los movimientos, hasta dar con una situación en la que se cumple el objetivo. Nada nuevo: es la BFS o el A* de la parte sobre búsqueda, solo que sobre estados escritos como conjuntos de hechos.
from collections import deque
def proved(stav, akce):
return (stav - akce.odeber) | akce.pridej
def planuj(start, cil, akce):
okraj, rodic = deque([start]), {start: None}
while okraj:
stav = okraj.popleft()
if cil <= stav: # se cumplen todos los átomos del objetivo
return plan_z(rodic, stav)
for a in akce:
if a.predpoklady <= stav:
dalsi = proved(stav, a)
if dalsi not in rodic:
rodic[dalsi] = (stav, a)
okraj.append(dalsi)
En el problema de la torre, la búsqueda en anchura encuentra el plan más corto, de 6 acciones, pero por el camino expande 2 132 estados: más de la mitad de los 4 051. Y es que no sabe nada de en qué dirección está el objetivo.
5. Heurísticas a partir de la relajación
En la parte sobre búsqueda, la heurística para el laberinto nos la inventamos nosotros. El planificador sabe calcularla a partir de la descripción de las acciones: simplifica el problema, lo resuelve y usa el coste de la solución simplificada como estimación.
Definición — relajación sin eliminaciones
El problema relajado se obtiene al poner del(a) = ∅ en todas las acciones. Un átomo que se cumple una vez se cumple para siempre.
El coste de un átomo p desde el estado s en el problema relajado se estima de forma recursiva: c(p) = 0 para p ∈ s; si no, c(p) = el mínimo, sobre las acciones a con p ∈ add(a), del valor 1 + c(pre(a)), donde el coste de un conjunto de átomos es
- en hmax, el máximo de los costes de sus átomos: la heurística es admisible,
- en hadd, la suma de los costes de sus átomos: una guía más precisa, pero puede sobrestimar.
La heurística del estado s es el coste del conjunto objetivo G.
c(p) = mina : p ∈ add(a) [ 1 + c(pre(a)) ]
- c(p)
- estimación de cuántas acciones hacen falta para que pase a cumplirse el átomo p (para los átomos que ya se cumplen es 0)
- mín. sobre las acciones con p ∈ add(a)
- un átomo puede introducirlo más de una acción; elegimos la más barata, porque el camino lo escoge el propio planificador
- 1
- el coste de la propia acción que finalmente añade p
- c(pre(a))
- el coste de que la acción sea siquiera aplicable: cumplir todas sus precondiciones. Aquí es donde hmax y hadd se diferencian: el máximo supone que las precondiciones se cumplen «a la vez» y que sus preparativos se solapan; la suma, que cada una cuesta un trabajo aparte
Imagina un mundo en el que nada se puede estropear: un bloque que has destapado una vez sigue destapado aunque le pongas algo encima. En un mundo así planificar es fácil, y su coste es una estimación optimista del coste real. hmax solo tiene en cuenta el subobjetivo más difícil (prudente, nunca sobrestima); hadd los suma todos (más atrevida, puede pasarse, porque a veces una sola acción ayuda a varios objetivos a la vez).
def ceny(stav, akce, agreguj): # agreguj = max (hmax) o sum (hadd)
c = {p: 0 for p in stav}
zmena = True
while zmena: # repetir mientras algún coste mejore
zmena = False
for a in akce:
if all(p in c for p in a.predpoklady):
nova = 1 + agreguj(c[p] for p in a.predpoklady)
for p in a.pridej: # a.odeber se ignora: relajación
if nova < c.get(p, float("inf")):
c[p], zmena = nova, True
return c
def h(stav, cil, akce, agreguj):
c = ceny(stav, akce, agreguj)
return agreguj(c[g] for g in cil) # objetivo inalcanzable: h = ∞
Ejemplo — hmax y hadd para la anomalía de Sussman
Inicio: C está sobre A y B está al lado; objetivo G = {sobre(A, B), sobre(B, C)} (el problema lo analizamos más abajo). Costes de los átomos por capas:
coste 0 (se cumplen): mesa(A), mesa(B), sobre(C, A), libre(B), libre(C)
coste 1: a la mesa(C, A) → mesa(C), libre(A); desde la mesa(B, C) → sobre(B, C); mover(C, A, B) → sobre(C, B)
coste 2: desde la mesa(A, B) necesita mesa(A), libre(A), libre(B), con costes 0, 1, 0
hmax: 1 + max(0, 1, 0) = 2, hadd: 1 + (0 + 1 + 0) = 2 → sobre(A, B) tiene coste 2
hmax(s0) = max(2, 1) = 2, hadd(s0) = 2 + 1 = 3, lo realmente necesario: h* = 3
hmax subestima porque solo cuenta el objetivo más caro: sobre(A, B) cuesta dos acciones, pero sobre(B, C) necesita además una tercera propia, y el máximo se la traga. La suma sí la cuenta y aquí acierta exactamente. En el problema de la torre se nota más: desde el inicio, h* = 6, hmax = 3, el número de objetivos sin cumplir es 5 y hadd = 15: la suma cuenta de nuevo para cada objetivo la preparación común (destapar bloques) y sobrestima el coste real dos veces y media.
Teorema — hmax y el número de objetivos sin cumplir son admisibles
Admisibilidad, como en la parte sobre búsqueda: h(s) ≤ h*(s), donde h*(s) es la longitud del plan más corto desde s.
hmax. Tomemos un plan más corto a1, …, ak del problema real. También es un plan del problema relajado (sin eliminaciones, en cada paso se cumplen al menos los mismos átomos, así que las precondiciones siguen cumpliéndose). Por inducción sobre i: todo átomo que se cumple tras i acciones tiene cmax ≤ i; o ya se cumplía antes, o lo añadió la acción ai, cuyas precondiciones se cumplían tras i − 1 acciones y tienen por tanto coste ≤ i − 1, de modo que 1 + max ≤ i. Todos los átomos del objetivo se cumplen tras k acciones, por lo que hmax(s) = maxg ∈ G c(g) ≤ k = h*(s).
Número de objetivos sin cumplir. Los objetivos del mundo de bloques son átomos sobre(·, ·), y cada acción añade como mucho uno de esos átomos. Para cumplir m objetivos que faltan hacen falta, por tanto, al menos m acciones.
Para hadd la demostración falla en el último paso: la suma de costes puede ser mayor que k, porque las mismas acciones del plan se cuentan para varios objetivos.
Tres heurísticas en el problema de la torre, todas con el algoritmo A*:
| Búsqueda | Estados expandidos | Longitud del plan |
|---|---|---|
| En anchura (sin heurística) | 2 132 | 6 |
| A* con hmax | 136 | 6 |
| A* con el número de objetivos sin cumplir | 19 | 6 |
| A* con hadd | 12 | 7 |
La admisible hmax encuentra el óptimo y ahorra más del 90 % del trabajo. El número de objetivos sin cumplir también es admisible en nuestro mundo (cada acción coloca un solo bloque, así que cumple como mucho un objetivo) y aquí funciona aún mejor. hadd es la que menos expande, pero lo paga: el plan tiene un paso más de lo necesario. Ninguna heurística gana siempre, y los planificadores reales, como FF o Fast Downward, tienen conjuntos enteros de heurísticas que combinan.
Cómo trabaja A* con hmax se ve mejor en un problema pequeño: la anomalía de Sussman, que analizaremos dos secciones más adelante. Cada fila es un estado expandido (torres de abajo arriba); A* toma siempre el estado con menor f = g + h, donde g es el número de acciones desde el inicio, y en caso de empate el que descubrió antes:
| Paso | Estado expandido | Última acción | g | hmax | f |
|---|---|---|---|---|---|
| 1 | A–C, B | — (inicio) | 0 | 2 | 2 |
| 2 | A, B, C | a la mesa(C, A) | 1 | 1 | 2 |
| 3 | A, B–C | mover(C, A, B) | 1 | 2 | 3 |
| 4 | A, C–B | desde la mesa(B, C) | 2 | 1 | 3 |
| 5 | C–B–A | desde la mesa(A, B) | 3 | 0 | 3 — meta |
La búsqueda en anchura expande en este mismo problema los 13 estados; A* con hmax, solo 5. En el tercer paso se desvía: pone C sobre B, porque ese estado tiene f = 3, igual que el camino correcto, y se descubrió antes. El paso siguiente lo corrige. El primer estado objetivo expandido tiene el plan más corto: eso lo garantiza A* cuando la heurística es admisible (y hmax es incluso consistente, así que no necesita volver a los estados cerrados).
6. Planificación hacia atrás
Definición — planificación hacia atrás (regresión)
Busca desde el objetivo: el estado de la búsqueda es un conjunto de átomos g que todavía deben cumplirse (al principio, g = G). Una acción a es relevante para g si add(a) ∩ g ≠ ∅ y del(a) ∩ g = ∅. La regresión del objetivo a través de a es
g′ = (g ∖ add(a)) ∪ pre(a)
- g′
- lo que debe cumplirse antes de la acción a para que después se cumpla g: un subobjetivo nuevo, anterior
- g
- el subobjetivo actual; los átomos que la acción no menciona deben cumplirse ya antes, porque la acción no los introduce (y, gracias a la condición de relevancia, tampoco los destruye)
- add(a)
- se restan, porque de estos átomos se encarga la propia acción: antes no tienen por qué cumplirse
- pre(a)
- se añaden, porque sin las precondiciones la acción no puede ejecutarse: pasan a ser nuevos subobjetivos
La búsqueda termina cuando g′ ⊆ s0; el plan son las acciones encontradas en orden inverso.
Te preguntas: «¿Qué acción podría darme lo que quiero sin destruir nada de ello? ¿Y qué tendría que cumplirse antes?». Es el mismo razonamiento que el encadenamiento hacia atrás de la parte anterior. Ventaja: solo examinas las acciones relacionadas con el objetivo. Inconveniente: trabajas con conjuntos de objetivos en lugar de estados completos, y estos son más difíciles de estimar.
Ejemplo — regresión del objetivo de la anomalía de Sussman
Objetivo g = {sobre(A, B), sobre(B, C)}, inicio s0 = {sobre(C, A), mesa(A), mesa(B), libre(B), libre(C)}. Tomamos las acciones empezando por la última:
desde la mesa(A, B): add = {sobre(A, B)} afecta a g, del = {mesa(A), libre(B)} no → relevante
g′ = {sobre(B, C)} ∪ {mesa(A), libre(A), libre(B)}
desde la mesa(B, C): add = {sobre(B, C)}, del = {mesa(B), libre(C)} → relevante
g″ = {mesa(A), libre(A), libre(B)} ∪ {mesa(B), libre(B), libre(C)} = {mesa(A), mesa(B), libre(A), libre(B), libre(C)}
a la mesa(C, A): add = {mesa(C), libre(A)}, del = {sobre(C, A)} → relevante
g‴ = {mesa(A), mesa(B), libre(B), libre(C)} ∪ {sobre(C, A), libre(C)} = {sobre(C, A), mesa(A), mesa(B), libre(B), libre(C)} ⊆ s0 ✓
El plan leído al revés: a la mesa(C, A), desde la mesa(B, C), desde la mesa(A, B). Pero si como última acción probáramos desde la mesa(B, C), obtendríamos el subobjetivo {sobre(A, B), mesa(B), libre(B), libre(C)}: A está sobre B y a la vez B está libre. Ese estado no existe, pero la regresión no lo detecta por sí sola; por eso los planificadores hacia atrás necesitan reglas adicionales que descubran las combinaciones imposibles.
7. La anomalía de Sussman
La idea más sencilla para planificar: dividir el objetivo en partes y cumplirlas una tras otra. Funciona a menudo, y falla ya con tres bloques. El problema lleva el nombre de Gerald Sussman, que lo analizó en los años setenta: C está sobre A, B está al lado, y el objetivo es A sobre B y, a la vez, B sobre C.
El plan más corto para «A sobre B» quita C y pone A sobre B. Pero entonces B queda debajo de A, y llevar B sobre C obliga a quitar A otra vez. Al final solo se cumple el segundo objetivo, y ha costado 4 pasos. El orden inverso acaba igual: ponemos B sobre C, pero así enterramos A bajo C y B, y liberarlo vuelve a destruir el primer objetivo. Los objetivos se estorban entre sí, empieces por el que empieces.
La solución necesita intercalar los pasos de ambos objetivos: primero liberar A (un paso para el primer objetivo), luego B sobre C (el segundo objetivo) y solo después A sobre B (el primer objetivo). Un planificador que busca en el espacio de estados completos lo encuentra solo: aquí recorrió los 13 estados del mundo de tres bloques. Un planificador que combina planes parciales tiene que saber desenredarlos y volver a unirlos. Ese es el tema de toda la familia de la «planificación en el espacio de planes».
8. Planificación jerárquica
Definición — HTN
En una red jerárquica de tareas (HTN, hierarchical task network) hay, además de acciones primitivas, tareas compuestas. Cada tarea compuesta tiene uno o más métodos: secuencias de subtareas en las que puede descomponerse. La planificación empieza con una tarea de nivel superior y la va descomponiendo hasta que solo quedan acciones primitivas.
Nadie planifica unas vacaciones paso a paso, pie tras pie. Las descompones en «llegar allí», «alojarse» y «volver»; «llegar» en «comprar el billete» e «ir a la estación», y solo abajo del todo están las operaciones concretas. Los métodos son recetas probadas para descomponer algo complicado, y gracias a ellos el planificador no prueba disparates.
def rozloz(ukoly, stav): # ukoly: lista de tareas pendientes
if not ukoly:
return [] # todo descompuesto
u, zbytek = ukoly[0], ukoly[1:]
if u.primitivni: # acción STRIPS corriente
if not u.predpoklady <= stav:
return None
plan = rozloz(zbytek, proved(stav, u))
return None if plan is None else [u] + plan
for metoda in u.metody: # probar las recetas una a una
if metoda.podminka <= stav:
plan = rozloz(metoda.podukoly + zbytek, stav)
if plan is not None:
return plan
return None # ningún método ha funcionado: retroceder
Aquí se busca en el espacio de descomposiciones, no de estados: solo se ramifica según qué método se elige para una tarea compuesta. Las tareas se descomponen en el orden en que se ejecutarán, así que el planificador siempre conoce el estado actual; así procede, por ejemplo, el planificador SHOP2.
La planificación jerárquica se usa allí donde los expertos tienen procedimientos probados: en logística, en el control de la producción y mucho en los videojuegos. El juego de acción F.E.A.R. (2005) se hizo famoso por sus soldados, que planificaban la táctica sobre la marcha con un lenguaje al estilo de STRIPS, y de la misma idea parten los planificadores de muchos otros juegos.
9. Planificar en un mundo incierto
STRIPS supone que el mundo es exactamente como creemos y que las acciones salen exactamente como dice su descripción. Pero un robot real deja caer el bloque y un servidor real no responde. Las respuestas más habituales a esto son tres:
- Supervisión y replanificación. Tras cada acción, comprueba si el mundo es como esperaba el plan. Si no, vuelve a planificar a partir de lo que hay. Sencillo y sorprendentemente eficaz (el bucle está más abajo).
- Los planes condicionales tienen ramificaciones: «si la puerta se abre, sigue adelante; si no, usa la otra entrada».
- Planificación probabilística. Cuando una acción tiene varios resultados posibles con probabilidades dadas, no buscamos una secuencia de acciones, sino una estrategia para cada situación; los procesos de decisión de Markov tienen su propia parte en el temario.
def proved_s_dohledem(cil, akce, svet):
plan = planuj(svet.pozoruj(), cil, akce)
while plan:
a = plan.pop(0)
cekany = proved(svet.pozoruj(), a) # lo que debería cumplirse según el modelo
svet.udelej(a)
skutecny = svet.pozoruj()
if cil <= skutecny:
return True
if skutecny != cekany: # el mundo se ha comportado de otra forma
plan = planuj(skutecny, cil, akce) # replanificar a partir de lo que hay
return cil <= svet.pozoruj()
Basta con vigilar solo aquello de lo que depende el resto del plan: si cambia un átomo que no es precondición de ninguna acción posterior ni forma parte del objetivo, el plan sigue valiendo. Este ahorro ya lo introdujo el sistema PLANEX, que ejecutaba los planes de Shakey (Fikes, Hart y Nilsson, 1972).
10. La planificación en los agentes de IA
Un agente basado en un modelo de lenguaje que corrige un error en el código también planifica: desglosa la tarea en una lista de pasos, tras cada paso mira el resultado (¿han pasado los tests?, ¿existe el archivo?) y ajusta el plan en consecuencia. Es supervisión y replanificación, solo que las acciones son comandos en la terminal y las precondiciones y los efectos no están escritos formalmente en ninguna parte: el modelo los estima a partir del texto.
Y precisamente ahí está el punto débil. La investigación con problemas del mundo de bloques (por ejemplo, el conjunto PlanBench) ha mostrado que los modelos de lenguaje se equivocan a menudo en la planificación clásica, sobre todo cuando los bloques se renombran con palabras sin sentido y el modelo no puede tirar de ejemplos aprendidos. Los modelos más recientes, con un razonamiento más largo, lo hacen mucho mejor, pero no garantizan que el resultado sea correcto. Por eso funciona bien repartir el trabajo: el modelo traduce el problema a PDDL o propone un plan, y un planificador clásico o un validador lo comprueba; cada uno hace lo que sabe hacer.
11. Resumen
- Planificar es buscar una secuencia de acciones hacia el objetivo; STRIPS describe el estado como un conjunto de átomos y la acción mediante precondiciones y átomos añadidos y eliminados.
- La planificación hacia delante es búsqueda en el espacio de estados; la planificación hacia atrás hace regresión del objetivo a través de las acciones relevantes.
- El planificador calcula por sí mismo la heurística a partir del problema relajado sin eliminaciones: hmax es admisible; hadd, más rápida, pero puede llevar a un plan más largo.
- La anomalía de Sussman muestra que los subobjetivos no siempre pueden cumplirse uno tras otro: hay que intercalar los pasos.
- La planificación jerárquica descompone las tareas según métodos probados; en un mundo incierto, el plan se supervisa y se replanifica, justo lo que hacen hoy los agentes de IA.
¿Quieres probarlo tú mismo? Pide a un agente de IA que escriba el mundo de bloques en PDDL y lo pase al planificador libre Fast Downward, y luego comprueba cuántos bloques es capaz de manejar el modelo solo, sin él.
¿Lo ves de otra manera? Escribe a support@coden-app.com.