Cerramos el módulo 5 con una constatación incómoda: las redes neuronales y los grandes modelos de lenguaje son subsimbólicos y opacos; nadie puede leer sus pesos ni pedirles cuentas de una decisión. Y NovaMarket tiene decisiones que exigen poder leerse y justificarse: si procede o no un reembolso (caso 8) o cuál es la causa probable de una incidencia (caso 9). Esta lección abre la otra mitad de la inteligencia artificial, la simbólica de las primeras décadas de 01-01, empezando por su cimiento: la lógica, el lenguaje formal para representar conocimiento de forma explícita y para razonar con él de forma verificable. Veremos la lógica proposicional (proposiciones, conectivos y tablas de verdad, equivalencias, reglas de inferencia, bases de conocimiento y la noción de implicación lógica), la lógica de primer orden (objetos, predicados, cuantificadores) que permite escribir las políticas de devolución de NovaMarket con precisión, y el mecanismo de trabajo de casi todos los sistemas basados en reglas: el encadenamiento hacia delante y hacia atrás con cláusulas de Horn. Todo con código en Python puro que puedes ejecutar y leer, línea a línea. Es importante porque la lógica es el vocabulario que compartirán las tres lecciones siguientes: los sistemas expertos (06-02) son lógica más ingeniería, las redes bayesianas (06-03) son la respuesta a lo que la lógica no sabe hacer, y las aplicaciones de 06-04 son lógica en producción.

Contenido

  1. Por qué la lógica: conocimiento explícito y verificable
  2. Lógica proposicional: proposiciones y conectivos
  3. Tautologías, contradicciones, satisfacibilidad y equivalencias útiles
  4. Reglas de inferencia: modus ponens, modus tollens y resolución
  5. Base de conocimiento y consulta: KB ⊨ α por enumeración de modelos
  6. Lógica de primer orden: objetos, predicados, funciones y cuantificadores
  7. Cláusulas de Horn: encadenamiento hacia delante y hacia atrás
  8. Prolog, el lenguaje de la lógica
  9. Limitaciones: mundo cerrado, monotonía e incertidumbre
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. Por qué la lógica: conocimiento explícito y verificable

En 02-02 distinguimos la IA simbólica (símbolos y reglas que un humano puede leer) de la subsimbólica (números y pesos que nadie lee). Los módulos 4 y 5 han sido subsimbólicos: la regresión logística de 04-04 y el MLP de 05-03 aprenden pesos de pedidos.csv, y el LLM de 05-05 genera texto sin poder explicar por qué. Para muchas tareas es lo adecuado. Pero considera lo que Diego necesita para las devoluciones:

  • La regla existe antes que los datos. El plazo de desistimiento de 14 días o la garantía legal de 3 años no se aprenden de un histórico: los fija la normativa y la política comercial de NovaMarket. Un modelo que "aprendiera" 13 o 16 días estaría sencillamente equivocado.
  • La decisión debe poder justificarse ante el cliente, ante un auditor y, como vimos en 02-04, ante el AI Act y el RGPD (decisiones automatizadas con efectos significativos). "El modelo dio 0,71" no es una justificación; "no procede porque el producto está usado y han pasado 20 días, regla R3" sí lo es.
  • Debe ser verificable: Diego tiene que poder leer las reglas, detectar que dos se contradicen y corregirlas, sin reentrenar nada.
  • Hay pocos datos y muchos casos límite (un producto de higiene abierto, una garantía ampliada), justo donde el ML rinde peor.

La lógica ofrece exactamente eso: un lenguaje formal con sintaxis precisa (qué frases están bien formadas), semántica precisa (qué significa que una frase sea verdadera) y reglas de inferencia que derivan conclusiones nuevas de forma mecánica y correcta. Es la herramienta que Aristóteles esbozó, Boole y Frege formalizaron y el Logic Theorist de 1956 llevó al ordenador (01-01). Empezamos por la versión más simple.

  1. Lógica proposicional: proposiciones y conectivos

Una proposición es una afirmación que es verdadera o falsa, sin término medio: "el producto está usado", "han pasado más de 14 días desde la entrega", "procede el reembolso". Las representamos con letras (P, Q, R) o con nombres legibles (usado, mas14, reembolso). Un modelo (o interpretación) es una asignación de verdadero/falso a cada proposición: describe un "mundo posible". Con 3 proposiciones hay 2³ = 8 mundos posibles.

Los conectivos combinan proposiciones en fórmulas compuestas. Su significado se define con tablas de verdad, que dicen el valor de la fórmula en cada combinación de valores de sus partes:

P Q ¬P (no P) P ∧ Q (y) P ∨ Q (o) P → Q (si P entonces Q) P ↔ Q (si y solo si)
V V F V V V V
V F F F V F F
F V V F V V F
F F V F F V V

Dos observaciones importantes para principiantes:

  • El o lógico es inclusivo: "P ∨ Q" es verdadera también cuando ambas lo son. "Producto dañado o equivocado" admite que sea las dos cosas.
  • La implicación P → Q solo es falsa cuando P es verdadera y Q falsa. Si P es falsa, la implicación es verdadera sea cual sea Q: la regla "si está usado y han pasado 14 días, no procede reembolso" no dice nada de los productos sin usar; no se incumple con ellos. Esto sorprende al principio, pero es exactamente lo que queremos de una regla: solo la viola el caso que cumple la condición y contradice la conclusión.

La regla de Diego con la que cerró el módulo 5 se escribe (usado ∧ mas14) → ¬reembolso. Su tabla de verdad tiene 8 filas y solo una es falsa: la del mundo en que el producto está usado, han pasado más de 14 días y aun así se reembolsa. Ese mundo es el que la regla prohíbe. Vamos a generar tablas como esta en código.

2.1 Código: evaluar fórmulas y generar tablas de verdad

Representamos una fórmula como un nombre de variable (cadena) o una tupla cuyo primer elemento es el operador: ("y", "P", "Q"), ("no", "R"), ("->", ("y", "P", "Q"), ("no", "R")). Un evaluador recursivo baja por la tupla y aplica la tabla de verdad de cada conectivo; itertools.product genera todas las combinaciones de V/F.

from itertools import product

def evaluar(formula, modelo):
    """Evalúa una fórmula proposicional dado un modelo (dict variable -> bool).
    Una fórmula es un nombre de variable (str) o una tupla (operador, operandos...)."""
    if isinstance(formula, str):
        return modelo[formula]                     # una proposición: su valor en el modelo
    op = formula[0]
    if op == "no":
        return not evaluar(formula[1], modelo)
    if op == "y":
        return evaluar(formula[1], modelo) and evaluar(formula[2], modelo)
    if op == "o":
        return evaluar(formula[1], modelo) or evaluar(formula[2], modelo)
    if op == "->":                                 # P -> Q equivale a (no P) o Q
        return (not evaluar(formula[1], modelo)) or evaluar(formula[2], modelo)
    if op == "<->":
        return evaluar(formula[1], modelo) == evaluar(formula[2], modelo)
    raise ValueError(f"operador desconocido: {op}")

def variables_de(formula, acc=None):
    """Recoge el conjunto de proposiciones que aparecen en la fórmula."""
    acc = set() if acc is None else acc
    if isinstance(formula, str):
        acc.add(formula)
    else:
        for sub in formula[1:]:
            variables_de(sub, acc)
    return acc

def tabla_verdad(formula, nombre="fórmula"):
    vars_ = sorted(variables_de(formula))
    print(" | ".join(f"{v:^5}" for v in vars_) + " | " + nombre)
    resultados = []
    for valores in product([True, False], repeat=len(vars_)):   # todas las combinaciones V/F
        modelo = dict(zip(vars_, valores))
        r = evaluar(formula, modelo)
        resultados.append(r)
        print(" | ".join(f"{'V' if x else 'F':^5}" for x in valores) + " | " + ("V" if r else "F"))
    if all(resultados):
        print("-> tautología")
    elif not any(resultados):
        print("-> contradicción")
    else:
        print(f"-> satisfacible ({sum(resultados)} de {len(resultados)} modelos)")

# P: "el producto está usado", Q: "han pasado más de 14 días", R: "procede el reembolso"
regla = ("->", ("y", "P", "Q"), ("no", "R"))
tabla_verdad(regla, "(P ∧ Q) → ¬R")

Salida:

  P   |   Q   |   R   | (P ∧ Q) → ¬R
  V   |   V   |   V   | F
  V   |   V   |   F   | V
  V   |   F   |   V   | V
  V   |   F   |   F   | V
  F   |   V   |   V   | V
  F   |   V   |   F   | V
  F   |   F   |   V   | V
  F   |   F   |   F   | V
-> satisfacible (7 de 8 modelos)

Explicación: evaluar es una función recursiva: si la fórmula es una cadena devuelve su valor en el modelo; si es una tupla, evalúa los operandos y combina los resultados con la tabla del conectivo. Fíjate en la línea de "->": implementamos la implicación como (no P) o Q, que es su definición. product([True, False], repeat=3) genera las 8 filas. La única fila falsa es la primera (usado, más de 14 días y reembolso), como habíamos razonado.

  1. Tautologías, contradicciones, satisfacibilidad y equivalencias útiles

Según cómo se comporte una fórmula en todos los modelos, la clasificamos:

Tipo Definición Ejemplo Uso práctico
Tautología Verdadera en todos los modelos P ∨ ¬P Las equivalencias y las leyes lógicas son tautologías
Contradicción Falsa en todos los modelos P ∧ ¬P Una base de reglas que la implica es inconsistente
Satisfacible Verdadera en al menos un modelo (P ∧ Q) → ¬R La mayoría de las reglas de negocio

Dos fórmulas son equivalentes (≡) si tienen la misma tabla de verdad, es decir, si su bicondicional es una tautología. Las equivalencias permiten reescribir reglas sin cambiar su significado, algo que hará el motor de 06-02 y que tú harás al depurar reglas de Diego:

Equivalencia Fórmula Lectura con NovaMarket
De Morgan ¬(P ∧ Q) ≡ ¬P ∨ ¬Q "No es cierto que esté usado y fuera de plazo" = "no está usado o está en plazo"
De Morgan ¬(P ∨ Q) ≡ ¬P ∧ ¬Q "No está ni dañado ni equivocado" = "no dañado y no equivocado"
Contrapositiva P → Q ≡ ¬Q → ¬P "Si es defectuoso, reembolso" = "si no hay reembolso, no era defectuoso"
Implicación P → Q ≡ ¬P ∨ Q Es lo que programamos en evaluar
Doble negación ¬¬P ≡ P
Distributiva P ∧ (Q ∨ R) ≡ (P ∧ Q) ∨ (P ∧ R) Desdoblar una regla con "o" en dos reglas

Cuidado con la conversa: P → Q no equivale a Q → P. "Si es defectuoso, procede reembolso" no significa "si procede reembolso, es defectuoso" (puede proceder por desistimiento). Es el error lógico más frecuente en reglas de negocio mal escritas.

Comprobamos De Morgan y una contradicción con el código anterior:

tabla_verdad(("<->", ("no", ("y", "P", "Q")), ("o", ("no", "P"), ("no", "Q"))), "¬(P∧Q) ↔ (¬P∨¬Q)")
tabla_verdad(("y", "P", ("no", "P")), "P ∧ ¬P")

Salida (resumida):

  P   |   Q   | ¬(P∧Q) ↔ (¬P∨¬Q)
  V   |   V   | V
  V   |   F   | V
  F   |   V   | V
  F   |   F   | V
-> tautología
  P   | P ∧ ¬P
  V   | F
  F   | F
-> contradicción

  1. Reglas de inferencia: modus ponens, modus tollens y resolución

Las tablas de verdad deciden cualquier pregunta, pero con n proposiciones tienen 2ⁿ filas: con las 40 proposiciones que puede manejar un sistema de devoluciones real son un billón de filas. Las reglas de inferencia evitan enumerar: son patrones que, aplicados a fórmulas verdaderas, producen fórmulas verdaderas (son correctas, sound).

Regla Premisas Conclusión Ejemplo NovaMarket
Modus ponens P → Q, P Q "Si es defectuoso y está en garantía, envío gratis"; es defectuoso y está en garantía; luego envío gratis
Modus tollens P → Q, ¬Q ¬P "Si el pedido salió de Getafe, el albarán lleva la letra G"; no lleva G; luego no salió de Getafe
Eliminación de ∧ P ∧ Q P De "dañado y con retraso" se sigue "dañado"
Introducción de ∧ P, Q P ∧ Q
Silogismo hipotético P → Q, Q → R P → R Encadenar reglas
Resolución P ∨ Q, ¬P ∨ R Q ∨ R Ver abajo

El modus ponens es la regla estrella de los sistemas expertos: dado "si condiciones entonces conclusión" y las condiciones, añade la conclusión. Es literalmente lo que hará el motor de la sección 7.

La resolución merece una intuición: si sabemos que "el paquete llegó dañado o el producto era el equivocado" (P ∨ Q) y también que "no llegó dañado o la causa es transporte" (¬P ∨ R), entonces, sea cual sea el valor de P, algo se salva: si P es cierta, la segunda cláusula obliga a R; si es falsa, la primera obliga a Q. Luego Q ∨ R. La resolución es una única regla que, aplicada sobre fórmulas convertidas a una forma normal de cláusulas, es completa: si algo se sigue lógicamente, la resolución lo encuentra (a menudo demostrando que la negación lleva a la cláusula vacía, una contradicción). Es el motor interno de Prolog (sección 8) y de los demostradores automáticos; aquí basta con la idea.

  1. Base de conocimiento y consulta: KB ⊨ α por enumeración de modelos

Una base de conocimiento (KB) es un conjunto de fórmulas que damos por verdaderas: reglas generales más hechos del caso. Una consulta α es una fórmula sobre la que preguntamos. Decimos que la KB implica lógicamente α, escrito KB ⊨ α, cuando α es verdadera en todos los modelos en que la KB es verdadera. Es la definición precisa de "se sigue de lo que sabemos": no queda ningún mundo compatible con lo que sabemos en el que α sea falsa.

La forma más directa de comprobarlo es la enumeración de modelos: recorrer todas las asignaciones, quedarse con las que satisfacen la KB y mirar si α es cierta en todas. Es exponencial, pero para bases pequeñas es perfecta y, sobre todo, es la definición hecha código.

def implica(kb, alfa):
    """Devuelve True si KB ⊨ alfa: en todo modelo donde todas las fórmulas de KB
    son verdaderas, alfa también lo es. Enumera todos los modelos."""
    vars_ = set()
    for f in kb + [alfa]:
        variables_de(f, vars_)
    vars_ = sorted(vars_)
    modelos_kb = 0
    for valores in product([True, False], repeat=len(vars_)):
        modelo = dict(zip(vars_, valores))
        if all(evaluar(f, modelo) for f in kb):        # ¿el modelo satisface la KB?
            modelos_kb += 1
            if not evaluar(alfa, modelo):              # ...y en él alfa es falsa: contraejemplo
                print(f"  contraejemplo: {modelo}")
                return False
    print(f"  ({modelos_kb} modelos de la KB, alfa cierta en todos)")
    return True

# Pedido 48377: auriculares NovaSound, devueltos usados a los 20 días
KB = [
    ("->", ("y", "usado", "mas14"), ("no", "reembolso")),   # regla de Diego
    ("->", "defectuoso", "reembolso"),                        # si es defectuoso, reembolso (garantía)
    "usado",                                                  # hecho
    "mas14",                                                  # hecho
]
print("KB ⊨ ¬reembolso ?", implica(KB, ("no", "reembolso")))
print("KB ⊨ ¬defectuoso ?", implica(KB, ("no", "defectuoso")))
print("KB ⊨ reembolso ?", implica(KB, "reembolso"))
KB2 = KB[:2] + ["usado"]           # ya no sabemos si han pasado 14 días
print("KB2 ⊨ ¬reembolso ?", implica(KB2, ("no", "reembolso")))

Salida:

  (1 modelos de la KB, alfa cierta en todos)
KB ⊨ ¬reembolso ? True
  (1 modelos de la KB, alfa cierta en todos)
KB ⊨ ¬defectuoso ? True
  contraejemplo: {'defectuoso': False, 'mas14': True, 'reembolso': False, 'usado': True}
KB ⊨ reembolso ? False
  contraejemplo: {'defectuoso': True, 'mas14': False, 'reembolso': True, 'usado': True}
KB2 ⊨ ¬reembolso ? False

Explicación, línea a línea del resultado:

  • KB ⊨ ¬reembolso: con usado y mas14 como hechos, la regla de Diego obliga a ¬reembolso en el único modelo compatible. Modus ponens hecho por fuerza bruta.
  • KB ⊨ ¬defectuoso: esto no lo habíamos escrito, y sin embargo se sigue: si fuera defectuoso habría reembolso (regla 2), pero no lo hay (regla 1); luego no es defectuoso. Es modus tollens, y es una alarma útil: la KB tal como está escrita afirma que un producto usado devuelto a los 20 días no puede ser defectuoso, lo cual es absurdo. La lógica ha detectado que las dos reglas, juntas, son incompatibles con un caso real (usado, tardío y defectuoso: la KB sería inconsistente, sin ningún modelo, y de una KB inconsistente se sigue cualquier cosa). En 06-02 lo resolveremos con prioridades entre reglas; aquí basta con notar que la verificación formal ha encontrado un error de diseño que un modelo opaco jamás mostraría.
  • KB ⊨ reembolso es falso, con un contraejemplo explícito: el mundo en que no es defectuoso y no hay reembolso es compatible con la KB.
  • KB2: al quitar el hecho mas14, ya no se puede concluir ¬reembolso; el contraejemplo muestra un mundo con mas14 falso, defectuoso y reembolsado. La consulta no está decidida: hace falta más información. Esa "pregunta que falta" será el motor de la interfaz de preguntas de 06-02.

  1. Lógica de primer orden: objetos, predicados, funciones y cuantificadores

La lógica proposicional trata "el producto está usado" como un átomo indivisible. Pero NovaMarket tiene 12.000 productos y 3.000 pedidos al día: no vamos a escribir una proposición usado_pedido_48377, usado_pedido_48378... La lógica de primer orden (LPO) añade estructura interna a las afirmaciones:

  • Objetos: cosas del mundo: el pedido 48377, el aspirador NovaClean, el cliente Ana López, el almacén de Getafe. Se nombran con constantes (p48377, getafe).
  • Predicados: propiedades y relaciones entre objetos, verdaderas o falsas: Producto(x), Precintado(x), Defectuoso(x), EntregadoDesde(p, almacen), Cliente(c, p).
  • Funciones: devuelven un objeto a partir de otros: DiasDesdeEntrega(p) devuelve un número, Almacen(p) devuelve un almacén.
  • Variables y cuantificadores: ∀x ("para todo x") y ∃x ("existe algún x").
  • Los mismos conectivos ¬, ∧, ∨, →, ↔ de antes.

Ahora la política de devoluciones de NovaMarket se puede escribir de una vez para todos los productos (los datos de negocio que fijamos y que reaparecerán en todo el módulo: desistimiento 14 días, garantía legal 3 años para producto nuevo, precintados/higiene no devolvibles si se abren, etiqueta original, envío de devolución gratuito si es defectuoso; la normativa concreta de consumo debe revisarla siempre un profesional legal, aquí es un ejemplo docente):

∀p  Producto(p) ∧ ¬Precintado(p) ∧ EtiquetaOriginal(p) ∧ DiasDesdeEntrega(p) ≤ 14  →  Devolvible(p)
∀p  Producto(p) ∧ Higiene(p) ∧ Abierto(p)  →  ¬Devolvible(p)
∀p  Producto(p) ∧ Nuevo(p) ∧ Defectuoso(p) ∧ DiasDesdeEntrega(p) ≤ 1095  →  EnGarantia(p)
∀p  EnGarantia(p)  →  EnvioDevolucionGratis(p)
∃p  Producto(p) ∧ Defectuoso(p) ∧ Almacen(p) = getafe          ("algún producto defectuoso salió de Getafe")

La primera regla es la que anuncia el enunciado de esta lección: Devolvible(p) ← Producto(p) ∧ ¬Precintado(p) ∧ DiasDesdeEntrega(p) ≤ 14, escrita con la flecha hacia la izquierda ("Devolvible si..."), la notación habitual de las reglas. Dos precisiones de lectura:

  • ∀ va con →: "para todo p, si p es un producto no precintado dentro de plazo, entonces es devolvible". Escribir ∀p (Producto(p) ∧ Devolvible(p)) diría que todo objeto es un producto devolvible: falso.
  • ∃ va con ∧: "existe p tal que p es producto y defectuoso y de Getafe". Escribir ∃p (Producto(p) → Defectuoso(p)) sería trivialmente cierto en cuanto exista algo que no sea producto (una implicación con antecedente falso es verdadera).

Las dos afirmaciones se relacionan por De Morgan cuantificado: ¬∀x P(x) ≡ ∃x ¬P(x) ("no todos los paquetes llegan bien" = "algún paquete no llega bien").

6.1 Unificación e inferencia, a nivel intuitivo

Para aplicar modus ponens a una regla con variables hace falta emparejarla con hechos concretos: unificar Producto(p) con Producto(p48377) sustituyendo p = p48377, comprobar ¬Precintado(p48377), EtiquetaOriginal(p48377) y DiasDesdeEntrega(p48377) ≤ 14 con la misma sustitución, y concluir Devolvible(p48377). La unificación es el algoritmo que encuentra la sustitución de variables que hace iguales dos expresiones (Cliente(c, p48377) unifica con Cliente(ana, p48377) con c = ana; Cliente(ana, p) no unifica con Cliente(luis, p48377)). Es la pieza que hace que una regla general se aplique a miles de casos, y el corazón de Prolog. En nuestro código nos ahorraremos la unificación con un truco habitual en la práctica: instanciar las reglas para un pedido concreto, es decir, convertir los predicados sobre el pedido en proposiciones (dentro_plazo, sin_usar), con lo que volvemos a la lógica proposicional, donde el modus ponens es un simple all(...).

  1. Cláusulas de Horn: encadenamiento hacia delante y hacia atrás

La inferencia general en LPO es cara (incluso indecidible en general). Los sistemas basados en reglas restringen las fórmulas a cláusulas de Horn: implicaciones con una conjunción de premisas positivas y una sola conclusión positiva, P₁ ∧ P₂ ∧ ... ∧ Pₙ → Q (un hecho es el caso n = 0). Con esa restricción existen dos algoritmos eficientes y correctos:

  • Encadenamiento hacia delante (forward chaining, dirigido por los datos): partes de los hechos conocidos, aplicas modus ponens a toda regla cuyas premisas ya estén satisfechas, añades la conclusión y repites hasta que no aparezca nada nuevo. Deriva todo lo derivable. Adecuado cuando llegan datos y quieres todas las consecuencias (un pedido llega devuelto y el sistema calcula reembolso, envío gratuito, aviso al proveedor...).
  • Encadenamiento hacia atrás (backward chaining, dirigido por el objetivo): partes de la pregunta ("¿procede reembolso?"), buscas reglas cuya conclusión sea ese objetivo, y conviertes sus premisas en nuevos subobjetivos, recursivamente, hasta llegar a hechos. Solo explora lo relevante para la pregunta y, si un hecho no está, puede preguntarlo al usuario: así funcionan los sistemas de diagnóstico interactivos de 06-02.

Reglas de devolución de NovaMarket como cláusulas de Horn (versión pequeña, sin conflictos entre reglas; los conflictos los trata 06-02):

Regla Premisas Conclusión
R1 dias_le_14 dentro_plazo
R2 dias_le_1095 en_garantia (3 años ≈ 1095 días, producto nuevo)
R3 dentro_plazo ∧ etiqueta_original ∧ sin_usar desistimiento_ok
R4 defectuoso ∧ en_garantia garantia_ok
R5 desistimiento_ok procede_reembolso
R6 garantia_ok procede_reembolso
R7 garantia_ok envio_devolucion_gratis

Traza hacia delante para el pedido 48213 (cafetera NovaBrew entregada hace 9 días, con etiqueta, sin usar, no defectuosa). Hechos iniciales: dias_le_14, dias_le_1095, etiqueta_original, sin_usar.

Paso Regla disparada Premisas satisfechas Hecho nuevo
1 R1 dias_le_14 dentro_plazo
2 R2 dias_le_1095 en_garantia
3 R3 dentro_plazo, etiqueta_original, sin_usar desistimiento_ok
4 R5 desistimiento_ok procede_reembolso
5 (R4, R6, R7 no se disparan: falta defectuoso) fin

Traza hacia atrás para el pedido 47102 (aspirador NovaClean, 40 días, usado, sin etiqueta, defectuoso), objetivo procede_reembolso:

Objetivo Regla probada Subobjetivos Resultado
procede_reembolso R5 desistimiento_ok →
desistimiento_ok R3 dentro_plazo, etiqueta_original, sin_usar →
dentro_plazo R1 dias_le_14 falla (no es hecho ni tiene regla) → R3 falla → R5 falla
procede_reembolso R6 garantia_ok →
garantia_ok R4 defectuoso, en_garantia defectuoso es hecho; →
en_garantia R2 dias_le_1095 es hecho → R2 ok → R4 ok → R6 ok → demostrado

Observa cómo el encadenamiento hacia atrás retrocede (backtracking) cuando una rama falla y prueba la siguiente regla con la misma conclusión.

7.1 Código: motor de encadenamiento hacia delante y hacia atrás

# Reglas de Horn: (nombre, [premisas], conclusión)
REGLAS = [
    ("R1", ["dias_le_14"], "dentro_plazo"),
    ("R2", ["dias_le_1095"], "en_garantia"),               # 3 años ≈ 1095 días
    ("R3", ["dentro_plazo", "etiqueta_original", "sin_usar"], "desistimiento_ok"),
    ("R4", ["defectuoso", "en_garantia"], "garantia_ok"),
    ("R5", ["desistimiento_ok"], "procede_reembolso"),
    ("R6", ["garantia_ok"], "procede_reembolso"),
    ("R7", ["garantia_ok"], "envio_devolucion_gratis"),
]

def hechos_de(pedido):
    """Traduce los datos del pedido a hechos (átomos verdaderos). Lo que no aparece
    se considera falso (suposición de mundo cerrado)."""
    h = set()
    if pedido["dias_desde_entrega"] <= 14:
        h.add("dias_le_14")
    if pedido["dias_desde_entrega"] <= 1095:
        h.add("dias_le_1095")
    if pedido["etiqueta_original"]:
        h.add("etiqueta_original")
    if not pedido["usado"]:
        h.add("sin_usar")
    if pedido["defectuoso"]:
        h.add("defectuoso")
    return h

def encadenar_adelante(hechos, reglas, verbose=True):
    hechos = set(hechos)
    aplicadas = []
    cambio = True
    while cambio:                       # repetir hasta que no se derive nada nuevo
        cambio = False
        for nombre, premisas, conclusion in reglas:
            if conclusion not in hechos and all(p in hechos for p in premisas):   # modus ponens
                hechos.add(conclusion)
                aplicadas.append(nombre)
                cambio = True
                if verbose:
                    print(f"  {nombre}: {' ∧ '.join(premisas)} → {conclusion}")
    return hechos, aplicadas

pedidos = {
    48213: dict(producto="Cafetera NovaBrew", dias_desde_entrega=9, etiqueta_original=True, usado=False, defectuoso=False),
    48377: dict(producto="Auriculares NovaSound", dias_desde_entrega=20, etiqueta_original=True, usado=True, defectuoso=False),
    47102: dict(producto="Aspirador NovaClean", dias_desde_entrega=40, etiqueta_original=False, usado=True, defectuoso=True),
}
for id_pedido, datos in pedidos.items():
    print(f"Pedido {id_pedido} ({datos['producto']}):")
    hechos = hechos_de(datos)
    print(f"  hechos iniciales: {sorted(hechos)}")
    finales, aplicadas = encadenar_adelante(hechos, REGLAS)
    print(f"  procede_reembolso: {'procede_reembolso' in finales}   reglas: {aplicadas}\n")

def demostrar(objetivo, hechos, reglas, nivel=0):
    """Encadenamiento hacia atrás: ¿se puede demostrar el objetivo?"""
    sangria = "  " * nivel
    if objetivo in hechos:
        print(f"{sangria}{objetivo}: es un hecho")
        return True
    for nombre, premisas, conclusion in reglas:
        if conclusion == objetivo:                       # regla que concluye el objetivo
            print(f"{sangria}{objetivo}: pruebo {nombre} (necesito {premisas})")
            if all(demostrar(p, hechos, reglas, nivel + 1) for p in premisas):
                return True                              # todas las premisas demostradas
    print(f"{sangria}{objetivo}: no demostrable")
    return False

print("Hacia atrás, pedido 47102:")
print(demostrar("procede_reembolso", hechos_de(pedidos[47102]), REGLAS))

Salida:

Pedido 48213 (Cafetera NovaBrew):
  hechos iniciales: ['dias_le_1095', 'dias_le_14', 'etiqueta_original', 'sin_usar']
  R1: dias_le_14 → dentro_plazo
  R2: dias_le_1095 → en_garantia
  R3: dentro_plazo ∧ etiqueta_original ∧ sin_usar → desistimiento_ok
  R5: desistimiento_ok → procede_reembolso
  procede_reembolso: True   reglas: ['R1', 'R2', 'R3', 'R5']

Pedido 48377 (Auriculares NovaSound):
  hechos iniciales: ['dias_le_1095', 'etiqueta_original']
  R2: dias_le_1095 → en_garantia
  procede_reembolso: False   reglas: ['R2']

Pedido 47102 (Aspirador NovaClean):
  hechos iniciales: ['defectuoso', 'dias_le_1095']
  R2: dias_le_1095 → en_garantia
  R4: defectuoso ∧ en_garantia → garantia_ok
  R6: garantia_ok → procede_reembolso
  R7: garantia_ok → envio_devolucion_gratis
  procede_reembolso: True   reglas: ['R2', 'R4', 'R6', 'R7']

Hacia atrás, pedido 47102:
procede_reembolso: pruebo R5 (necesito ['desistimiento_ok'])
  desistimiento_ok: pruebo R3 (necesito ['dentro_plazo', 'etiqueta_original', 'sin_usar'])
    dentro_plazo: pruebo R1 (necesito ['dias_le_14'])
      dias_le_14: no demostrable
    dentro_plazo: no demostrable
  desistimiento_ok: no demostrable
procede_reembolso: pruebo R6 (necesito ['garantia_ok'])
  garantia_ok: pruebo R4 (necesito ['defectuoso', 'en_garantia'])
    defectuoso: es un hecho
    en_garantia: pruebo R2 (necesito ['dias_le_1095'])
      dias_le_1095: es un hecho
True

Explicación:

  • REGLAS es una lista de tuplas: nombre, lista de premisas y conclusión. Es toda la "base de conocimiento" de reglas; los hechos de cada pedido los produce hechos_de, que instancia los predicados de primer orden (DiasDesdeEntrega(p) ≤ 14) en proposiciones (dias_le_14) para ese pedido concreto, como anunciamos en 6.1.
  • encadenar_adelante es un bucle "hasta que nada cambie": en cada vuelta recorre las reglas y dispara (modus ponens) las que tienen todas las premisas en el conjunto de hechos y cuya conclusión aún no está. Devuelve los hechos finales y la lista de reglas aplicadas, que ya es una explicación rudimentaria ("procede reembolso porque R1, R3, R5"). En 06-02 la enriqueceremos.
  • Compara los tres pedidos: la cafetera se reembolsa por desistimiento; los auriculares usados y tardíos no derivan procede_reembolso (nada lo demuestra, y con mundo cerrado eso significa "no"); el aspirador defectuoso a los 40 días se reembolsa por garantía y además tiene envío gratuito (R7), aunque no lleve etiqueta ni esté sin usar: la garantía es otra vía distinta del desistimiento, exactamente como en la política real.
  • demostrar es el encadenamiento hacia atrás en diez líneas: si el objetivo es un hecho, listo; si no, prueba cada regla que lo concluye y demuestra recursivamente sus premisas; all(...) se detiene en la primera premisa que falla (retroceso). La traza impresa reproduce la tabla de la sección 7 y muestra que solo se ha explorado lo relevante para la pregunta.

  1. Prolog, el lenguaje de la lógica

Todo lo anterior es tan regular que existe un lenguaje de programación cuyo intérprete es un motor de encadenamiento hacia atrás con unificación y resolución sobre cláusulas de Horn: Prolog (1972). Las mismas reglas se escriben casi igual que en la tabla, y la consulta es una pregunta:

% Hechos y reglas (una cláusula por línea; ":-" se lee "si")
dias_desde_entrega(p48213, 9).
etiqueta_original(p48213).
sin_usar(p48213).
dentro_plazo(P) :- dias_desde_entrega(P, D), D =< 14.
desistimiento_ok(P) :- dentro_plazo(P), etiqueta_original(P), sin_usar(P).
procede_reembolso(P) :- desistimiento_ok(P).
% Consulta interactiva:
% ?- procede_reembolso(p48213).      -> true.
% ?- procede_reembolso(X).           -> X = p48213.

No lo ejecutaremos aquí (no forma parte del entorno del curso); lo mencionamos porque es el ejemplo puro de programación declarativa: describes qué es cierto y el intérprete busca cómo demostrarlo. En 07-01 compararemos Prolog con Python y con Lisp, y en 06-02 y 06-04 veremos las herramientas modernas (CLIPS, Drools, experta) que heredan estas ideas.

  1. Limitaciones: mundo cerrado, monotonía e incertidumbre

La lógica es precisa, pero esa precisión tiene un precio. Conviene conocer sus tres límites principales antes de construir un sistema con ella:

  • Suposición de mundo cerrado: en nuestro motor, lo que no está en los hechos se trata como falso. Para el pedido 48377 concluimos "no procede reembolso" porque no pudimos demostrar que procediera, no porque demostráramos lo contrario. En bases de datos y reglas de negocio la suposición es cómoda (si un producto no consta como defectuoso, no lo es), pero puede ser peligrosa: "no consta que sea defectuoso" y "sabemos que no es defectuoso" son cosas distintas. La lógica pura (secciones 2-5) es de mundo abierto: si no se sabe, no se concluye nada; recuerda KB2 en la sección 5.
  • Monotonía: en lógica clásica, añadir conocimiento nunca elimina conclusiones. Pero el sentido común no funciona así: "los productos electrónicos tienen 3 años de garantía" y luego "salvo si son reacondicionados (1 año)" nos obliga a retirar una conclusión al saber más. Los sistemas expertos lo resuelven de forma pragmática con excepciones y prioridades entre reglas (06-02); la investigación en lógicas no monótonas es amplia y queda fuera del curso.
  • Incertidumbre: la lógica es binaria. "El paquete llega abollado" no implica con certeza "daño en transporte": también puede ser un error de picking con embalaje deficiente. Los síntomas de una incidencia son ambiguos, y forzarlos a verdadero/falso pierde información. Precisamente el diagnóstico de incidencias del caso 9 necesita grados de creencia, y ese es el tema de 06-03: probabilidad, teorema de Bayes y redes bayesianas.

Y una limitación práctica que no es de la lógica sino del proceso: alguien tiene que escribir las reglas correctamente, mantenerlas y validarlas con casos. Ese trabajo se llama ingeniería del conocimiento y es el corazón de la próxima lección.

Errores Comunes y Consejos

  • Confundir la implicación con la conversa. "Si defectuoso entonces reembolso" no autoriza a deducir "defectuoso" de "reembolso". Cuando escribas una regla, lee también su conversa en voz alta y comprueba que no la estás asumiendo.
  • Pensar que P → Q es falsa cuando P es falsa. Una regla no se incumple en los casos que no cumplen su condición. La única fila falsa de la implicación es V → F.
  • Escribir ∀ con ∧ o ∃ con →. Es el error de sintaxis más común en primer orden: "para todo p, si Producto(p) entonces..." y "existe p tal que Producto(p) y...".
  • Usar tablas de verdad para todo. Son perfectas para entender y para verificar bases pequeñas (nuestro implica), pero crecen como 2ⁿ. Para bases reales, encadenamiento con Horn o resolución.
  • Olvidar la suposición de mundo cerrado. Si tu motor devuelve "no procede", pregúntate si es "demostrado que no" o "no demostrado". Para decisiones con efectos sobre personas (02-04), la segunda debería llevar a "preguntar más" o a revisión humana, no a denegar.
  • No comprobar la consistencia. Nuestra KB de la sección 5 implicaba ¬defectuoso, señal de que dos reglas chocaban en un caso real. Ejecuta consultas "absurdas" (¿implica la KB que ningún producto es defectuoso?) para cazar estos choques antes de que los cace un cliente.
  • Nombres de proposiciones ambiguos. plazo no dice si es "dentro" o "fuera". Usa nombres que sean afirmaciones completas y positivas (dentro_plazo, sin_usar): las cláusulas de Horn no admiten negaciones en las premisas y agradecerás la claridad.

Ejercicios

Ejercicio 1. Con tabla_verdad, comprueba si son tautologías: (a) la contrapositiva (P → Q) ↔ (¬Q → ¬P); (b) la conversa (P → Q) ↔ (Q → P); (c) el silogismo hipotético ((P → Q) ∧ (Q → R)) → (P → R). Interpreta cada resultado con proposiciones de NovaMarket (P: defectuoso, Q: procede reembolso, R: envío gratuito).

Ejercicio 2. Amplía la base KB de la sección 5 con la regla de la garantía tal como debería ser: si es defectuoso y está en garantía, procede reembolso (en lugar de "si es defectuoso, procede reembolso"), y añade el hecho en_garantia. Con implica, comprueba de nuevo si KB ⊨ ¬defectuoso. ¿Ha cambiado algo? ¿Qué te dice eso sobre la incompatibilidad detectada, y qué haría falta para resolverla de verdad? (No hace falta programar la solución; basta con razonarla: se hará en 06-02.)

Ejercicio 3. Añade a REGLAS de la sección 7 la política de productos de higiene: R8 higiene ∧ abierto → no_devolvible, y haz que hechos_de genere higiene y abierto a partir de dos nuevas claves del pedido. Ejecuta el encadenamiento hacia delante con un cepillo eléctrico NovaSmile abierto y devuelto a los 5 días con etiqueta. ¿Qué deriva el motor? ¿Es coherente? Explica por qué el motor de esta lección no puede resolver lo que ves y qué mecanismo hará falta.

Soluciones

Solución 1. (a) Tautología: las 4 filas son V; "si es defectuoso procede reembolso" equivale a "si no procede reembolso, no era defectuoso". (b) No es tautología (satisfacible: falla cuando P y Q difieren): que proceda el reembolso no implica que sea defectuoso (puede ser desistimiento). (c) Tautología (8 filas, todas V): las reglas se encadenan; es la base de que el encadenamiento hacia delante sea correcto: si defectuoso → reembolso y reembolso → aviso, entonces defectuoso → aviso.

Solución 2. Con ("->", ("y", "defectuoso", "en_garantia"), "reembolso") y el hecho en_garantia, implica(KB, ("no", "defectuoso")) sigue devolviendo True (un único modelo de la KB, en el que defectuoso es falso). No ha cambiado nada, porque el conflicto de fondo no era la formulación de la garantía sino que las dos reglas concluyen cosas contrarias (¬reembolso y reembolso) sobre un mismo pedido usado, tardío, en garantía y defectuoso; la lógica clásica no puede tener ambas y "sacrifica" la posibilidad de que sea defectuoso. Resolverlo de verdad exige que una regla prevalezca sobre la otra (la garantía sobre el desistimiento) o reescribir la regla de Diego con una excepción (usado ∧ mas14 ∧ ¬defectuoso → ¬reembolso), lo que en cláusulas de Horn no se puede expresar directamente (premisa negada). Los sistemas expertos lo hacen con prioridades y resolución de conflictos, tema de 06-02.

Solución 3. Con ("R8", ["higiene", "abierto"], "no_devolvible") y el pedido dict(dias_desde_entrega=5, etiqueta_original=True, usado=False, defectuoso=False, higiene=True, abierto=True) (y hechos_de añadiendo higiene/abierto cuando las claves sean verdaderas), el motor deriva a la vez procede_reembolso (por R1, R3, R5: está en plazo, con etiqueta y "sin usar") y no_devolvible (por R8). Es incoherente: un producto de higiene abierto no debe reembolsarse por desistimiento. El motor de esta lección solo añade hechos y no tiene forma de decir que R8 bloquea a R3/R5: las cláusulas de Horn no tienen negación en las premisas ni noción de prioridad. Hará falta un motor con resolución de conflictos (prioridad o especificidad: R8 es más específica que R3) y una memoria de trabajo que registre una única decision, que es lo que construiremos en 06-02.

Conclusión

En esta lección hemos abierto la mitad simbólica de la IA con su herramienta fundamental. Hemos visto por qué NovaMarket necesita conocimiento explícito y verificable para las devoluciones (las reglas preceden a los datos, deben justificarse y auditarse); la lógica proposicional con sus cinco conectivos y sus tablas de verdad, generadas con itertools.product; las nociones de tautología, contradicción y satisfacibilidad, y las equivalencias (De Morgan, contrapositiva) que permiten reescribir reglas sin cambiar su significado; las reglas de inferencia (modus ponens, modus tollens, resolución) que evitan enumerar; la implicación lógica KB ⊨ α, comprobada por enumeración de modelos, que además nos ha destapado un choque entre la regla de Diego y la de la garantía; la lógica de primer orden, con la que la política de devoluciones se escribe una vez para los 12.000 productos, y la unificación que la aplica a cada pedido; y los dos algoritmos de trabajo sobre cláusulas de Horn, el encadenamiento hacia delante (dirigido por los datos, encadenar_adelante) y hacia atrás (dirigido por el objetivo, demostrar), que han decidido el reembolso de la cafetera NovaBrew, los auriculares NovaSound y el aspirador NovaClean con una traza legible. Hemos cerrado con Prolog y con los límites de la lógica: mundo cerrado, monotonía e incertidumbre.

Ya tenemos el motor mínimo. Lo que falta es todo lo que convierte un motor en un sistema que Diego pueda usar: reglas con prioridad que resuelvan los conflictos que hemos dejado abiertos (la garantía frente al desistimiento, el producto de higiene abierto), una memoria de trabajo, un módulo que explique "por qué" y "cómo", una interfaz que pregunte al usuario los hechos que faltan y un proceso para extraer las reglas de la cabeza del experto y validarlas. Eso es un sistema experto, la tecnología que en los años 80 sacó la IA de los laboratorios (MYCIN, XCON, 01-01), y lo construiremos en Python puro para las devoluciones y garantías de NovaMarket en la próxima lección, 06-02.

Fundamentos de Inteligencia Artificial (IA)

Módulo 1: Introducción a la Inteligencia Artificial

Módulo 2: Principios Básicos de la IA

Módulo 3: Algoritmos en IA

Módulo 4: Aprendizaje Automático (Machine Learning)

Módulo 5: Redes Neuronales y Deep Learning

Módulo 6: Lógica y Sistemas Expertos

Módulo 7: Herramientas y Lenguajes de Programación en IA

Módulo 8: Proyectos y Casos de Estudio

Módulo 9: Ejercicios y Prácticas

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados