En 06-01 construimos un motor de encadenamiento de diez líneas y dejamos dos problemas abiertos: dos reglas que chocan (la garantía frente a "usado y más de 14 días") y un cepillo de higiene abierto que el motor reembolsaba y denegaba a la vez. Resolverlos exige más que lógica: hace falta decidir qué regla gana, recordar qué se sabe del caso, explicar cada conclusión, preguntar al usuario lo que falta y, antes de todo eso, sacar las reglas de la cabeza de Diego y comprobarlas. Ese conjunto de piezas es un sistema experto, la tecnología que en los años 80 llevó la IA a la industria (01-01) y que hoy sobrevive, rebautizada, en los motores de reglas de negocio de bancos, aseguradoras y tiendas online. En esta lección veremos qué es un sistema experto y de dónde viene, su arquitectura pieza a pieza (base de conocimiento, motor de inferencia con resolución de conflictos, memoria de trabajo, explicación, adquisición del conocimiento e interfaz), cómo se construye uno (ingeniería del conocimiento con el experto, que en NovaMarket es Diego), sus ventajas y límites, y cómo se compara con un modelo de ML. Y construiremos en Python puro un sistema experto de devoluciones y garantías (caso 8) con prioridades, traza de explicación y una interfaz que pregunta lo que necesita. Es importante porque es el patrón que usarás cada vez que una decisión deba ser explicable, auditable y modificable sin reentrenar.
Contenido
- Qué es un sistema experto y de dónde viene
- Arquitectura de un sistema experto
- El motor de inferencia: encadenamiento y resolución de conflictos
- Explicación: ¿por qué? y ¿cómo?
- Construir un sistema experto: ingeniería del conocimiento
- Ventajas y desventajas; comparación con un modelo de ML
- Código: sistema experto de devoluciones y garantías de NovaMarket
- Herramientas reales
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué es un sistema experto y de dónde viene
Un sistema experto es un programa que resuelve problemas de un dominio concreto al nivel de un experto humano, usando conocimiento explícito (hechos y reglas escritas por personas) y un mecanismo de razonamiento separado de ese conocimiento. La separación es la idea clave: el motor no sabe nada de devoluciones; las reglas no saben nada de cómo se ejecutan. Cambiar la política es cambiar reglas, no código.
Los tres hitos históricos que ya citamos en 01-01, con lo que cada uno aportó:
- DENDRAL (Stanford, 1965-): identificaba estructuras moleculares a partir de espectros de masas. Demostró que el conocimiento específico del dominio, y no un razonador general, era lo que daba resultados: "en el conocimiento está el poder" (Feigenbaum).
- MYCIN (Stanford, años 70): diagnóstico de infecciones bacterianas y recomendación de antibióticos con unas 600 reglas SI-ENTONCES, encadenamiento hacia atrás que preguntaba al médico, un módulo que explicaba "por qué" preguntaba y "cómo" había llegado a la conclusión, y factores de certeza para la incertidumbre (06-03). Su motor vacío, EMYCIN, fue la primera shell reutilizable.
- XCON / R1 (Digital Equipment Corporation, 1980): configuraba pedidos de ordenadores VAX con miles de reglas de encadenamiento hacia delante; ahorró decenas de millones de dólares al año y disparó el auge comercial. Sus problemas de mantenimiento (más de 10.000 reglas hacia 1988, con equipos enteros dedicados a mantenerlas) anticiparon el invierno.
Lo que NovaMarket quiere para las devoluciones está más cerca de XCON (reglas de negocio aplicadas a cada pedido) que de MYCIN, pero tomará de MYCIN la explicación y las preguntas al usuario.
- Arquitectura de un sistema experto
flowchart LR
subgraph SE[Sistema experto de devoluciones]
BC[(Base de conocimiento<br/>hechos generales + reglas R1..R8)]
MT[(Memoria de trabajo<br/>hechos del caso: pedido 47102)]
MI[Motor de inferencia<br/>encadenamiento + resolución de conflictos]
EX[Módulo de explicación<br/>¿por qué? / ¿cómo?]
AQ[Módulo de adquisición<br/>del conocimiento]
end
UI[Interfaz de usuario<br/>agente de atención al cliente]
D[Diego, experto<br/>en devoluciones]
IC[Ingeniero/a del conocimiento<br/>Marta]
UI <--> MI
UI <--> EX
MI <--> BC
MI <--> MT
EX --> MT
EX --> BC
D --> IC --> AQ --> BC
Las piezas, una a una:
| Componente | Qué contiene / hace | En NovaMarket |
|---|---|---|
| Base de conocimiento | Conocimiento permanente del dominio: reglas SI-ENTONCES y hechos generales. No cambia de un caso a otro | Las reglas de devoluciones y garantías: plazo 14 días, garantía 3 años, excepciones de higiene/precintados, envío gratuito si defectuoso |
| Memoria de trabajo | Hechos del caso actual: los datos de entrada, las respuestas del usuario y las conclusiones intermedias. Se vacía en cada consulta | Pedido 47102: 40 días, usado, defectuoso; luego decision = garantia |
| Motor de inferencia | Aplica las reglas a la memoria de trabajo (encadenamiento hacia delante o atrás, 06-01) y decide qué regla disparar cuando varias son aplicables (resolución de conflictos) | Nuestra clase MotorInferencia |
| Módulo de explicación | Responde "¿por qué me preguntas esto?" (qué regla intenta aplicar) y "¿cómo has llegado a esa conclusión?" (cadena de reglas y hechos) | La traza "se aplicó R3 porque..." |
| Módulo de adquisición del conocimiento | Herramientas para introducir, editar y validar reglas sin programar (editores, comprobación de consistencia, pruebas con casos) | Una hoja de cálculo o formulario donde Diego edita reglas y ve qué casos de prueba cambian |
| Interfaz de usuario | Recoge los datos, formula preguntas, muestra la decisión y la explicación | La pantalla del agente de atención al cliente o el asistente del caso 7 |
Fíjate en la línea que separa la base de conocimiento de la memoria de trabajo: es la misma que en 06-01 separaba REGLAS (general) de hechos_de(pedido) (particular). Y en que el experto (Diego) no toca directamente la base: lo hace a través del ingeniero del conocimiento (Marta) y del módulo de adquisición.
- El motor de inferencia: encadenamiento y resolución de conflictos
El motor repite un ciclo de reconocimiento-acción:
- Emparejar: encontrar todas las reglas cuyas condiciones se cumplen con la memoria de trabajo actual. El resultado es el conjunto de conflicto.
- Resolver el conflicto: elegir una regla del conjunto (si hay varias).
- Actuar: disparar la regla elegida: añadir sus conclusiones a la memoria de trabajo (o preguntar, o ejecutar una acción).
- Volver a 1 hasta que no haya reglas aplicables o se alcance el objetivo.
En 06-01 el paso 2 no existía: disparábamos todas en orden de escritura, y de ahí venían las incoherencias. Las estrategias clásicas de resolución de conflictos, que se suelen combinar en este orden:
| Estrategia | Criterio | Ejemplo en devoluciones |
|---|---|---|
| Refractoriedad | Una regla no se dispara dos veces sobre los mismos hechos | Evita bucles infinitos |
| Prioridad (salience) | Gana la regla con mayor prioridad, asignada por el experto | La garantía (R1, prioridad 10) gana a la regla de Diego (R8, prioridad 2): un producto defectuoso se atiende aunque hayan pasado 14 días |
| Especificidad | Gana la regla con más condiciones (más específica): las excepciones son más específicas que la regla general | "En plazo, sin usar, con etiqueta y con 5+ devoluciones previas → revisión humana" (R5) gana a "en plazo, sin usar, con etiqueta → reembolso" (R4) |
| Recencia | Gana la regla que usa los hechos más recientes: sigue el hilo del razonamiento | Tras responder el usuario una pregunta, se prefieren las reglas que la usan |
| Orden | La primera escrita | Último recurso; frágil |
Además hay una decisión de diseño sobre cuándo parar: derivar todo lo derivable (útil para calcular todas las consecuencias de un pedido) o parar al fijar el objetivo (decision), que es lo que haremos: una vez que la regla ganadora ha fijado la decisión, las de menor prioridad ya no pueden contradecirla. Es la forma más simple de que "gane" la regla correcta sin negaciones en las premisas.
- Explicación: ¿por qué? y ¿cómo?
Es la funcionalidad que distingue un sistema experto de un simple if gigante y la razón por la que este módulo existe tras 05-05. MYCIN respondía a dos preguntas, y nosotros implementaremos las dos:
- ¿Por qué? (durante la consulta): "¿por qué me preguntas si el producto muestra señales de uso?" → "porque intento aplicar R5, que necesita saber si está usado". Da contexto al usuario y le permite detectar preguntas absurdas (síntoma de reglas mal escritas).
- ¿Cómo? (al final): "¿cómo has decidido que procede garantía?" → "se aplicó R1 porque defectuoso = True, producto nuevo = True y 40 días ≤ 1095; R1 dice: garantía legal de 3 años; el envío lo paga NovaMarket". Es la justificación que exige el cliente, el auditor y, en 02-04, el RGPD para decisiones automatizadas.
Técnicamente, la explicación se obtiene casi gratis si el motor registra para cada hecho de la memoria de trabajo quién lo puso (dato de entrada, respuesta del usuario o nombre de regla) y guarda una traza de las reglas disparadas con los valores concretos que satisfacían sus condiciones. Con eso, "¿por qué?" es seguir la cadena de orígenes hacia atrás.
- Construir un sistema experto: ingeniería del conocimiento
Un sistema experto no se entrena: se construye, y el proceso se llama ingeniería del conocimiento. Con NovaMarket como ejemplo:
- Elegir el dominio y el alcance. Estrecho, bien definido, con un experto disponible y decisiones repetitivas: las devoluciones y garantías (caso 8) cumplen; "atención al cliente en general" no.
- Adquisición del conocimiento: entrevistas al experto. Marta se sienta con Diego. Técnicas: preguntas abiertas ("¿cómo decides si procede?"), casos concretos ("¿y este pedido de la semana pasada?"), think aloud (Diego resuelve casos en voz alta), revisión de documentos (política de devoluciones, normativa de consumo, la cual, insistimos, debe revisar un profesional legal). Es la fase más lenta: los expertos saben más de lo que saben decir (cuello de botella de la adquisición), olvidan las excepciones hasta que ven un caso, y a veces se contradicen entre sesiones.
- Formalizar: extraer reglas. Convertir lo dicho en condiciones y conclusiones precisas, con nombres de atributos definidos en un diccionario (¿"usado" significa "con señales de uso" o "desprecintado"? Diego distingue las dos cosas y el sistema debe hacerlo también). Asignar prioridades y justificaciones. Detectar reglas incompletas ("¿y si está usado pero es defectuoso?").
- Validar con casos. Un conjunto de casos de prueba con la decisión que Diego considera correcta (20-50 casos reales del histórico de devoluciones), que se ejecuta cada vez que cambia una regla: el equivalente del conjunto de test de 04-05. Buscar reglas que nunca se disparan, casos sin decisión, y decisiones que Diego no firmaría.
- Mantener. Las políticas cambian (una campaña con 30 días de devolución en Navidad, una nueva categoría), la normativa cambia y aparecen casos nuevos. Cada cambio pasa por el módulo de adquisición y por la batería de casos. XCON murió de esto: miles de reglas sin dueño claro. La receta: reglas pocas, nombradas, con justificación escrita y propietario (Diego es el propietario de las reglas de devoluciones, como acordamos en 02-03 para los datos).
Una observación que conecta con el módulo 4: el paso 4 se parece a evaluar un modelo, y el paso 3 se parece a lo que hace un árbol de decisión, pero al revés: allí el algoritmo encuentra las reglas en pedidos.csv; aquí las dicta el experto. Y hay un puente evidente: el export_text del árbol de 04-04 produce reglas legibles que pueden servir de borrador para la entrevista con Diego ("el árbol dice que las devoluciones con más de 3 previas y menos de 5 días son sospechosas; ¿te cuadra?").
- Ventajas y desventajas; comparación con un modelo de ML
| Ventajas | Desventajas |
|---|---|
| Explicable por construcción: cada decisión tiene su cadena de reglas | Cuello de botella de la adquisición: extraer y formalizar el conocimiento es lento y caro |
| Modificable sin reentrenar: cambiar una regla es un cambio de política, no de código | Frágil fuera del dominio: no sabe nada de lo que no está en las reglas; no degrada con elegancia |
| No necesita datos históricos: funciona desde el primer día y con casos raros | Mantenimiento difícil al crecer: interacciones entre cientos de reglas, prioridades enredadas |
| Consistente: la misma entrada, la misma salida, siempre | Incertidumbre mal soportada en su forma pura (06-03) |
| Auditable y conforme con normativa (AI Act, RGPD, 02-04) | No aprende de la experiencia salvo que alguien reescriba reglas |
| Conserva y difunde el conocimiento del experto (si Diego se va, las reglas se quedan) | Codifica también los sesgos y errores del experto |
Frente a un modelo de ML (el árbol o la logística de 04-04):
| Aspecto | Sistema experto (reglas escritas) | Modelo de ML (reglas aprendidas) |
|---|---|---|
| Origen del conocimiento | Un experto y la normativa | Datos históricos etiquetados |
| Necesita datos | No (sí casos de prueba) | Sí, muchos y representativos |
| Explicabilidad | Total | Del árbol sí; de la red no |
| Cambiar la política | Editar una regla | Recoger datos nuevos y reentrenar |
| Casos raros y excepciones | Se escriben una vez | Mal cubiertos si son raros en los datos |
| Patrones sutiles en muchos datos | No los ve | Su punto fuerte |
| Incertidumbre | Añadida (06-03) | Nativa: probabilidades |
| Ejemplo NovaMarket | Devoluciones y garantías (caso 8), políticas fijadas por normativa | Riesgo de fraude (caso 3), demanda (caso 2), reseñas (caso 4) |
Cuándo cada uno: reglas cuando la decisión está definida por normativa o política, debe justificarse una a una, hay pocos datos o los casos límite importan; ML cuando la relación entrada-salida no la sabe escribir nadie pero está en los datos. Y muy a menudo los dos: el ML estima (probabilidad de fraude, de defecto) y las reglas deciden con esa estimación como un hecho más. Es la combinación neurosimbólica que anunció 05-05 y que cerrará el módulo en 06-04.
- Código: sistema experto de devoluciones y garantías de NovaMarket
Construimos las piezas de la sección 2 en Python puro. Los hechos de la memoria de trabajo son pares atributo-valor (dias_desde_entrega = 40, usado = True), y las condiciones de una regla son tuplas (atributo, operador, valor), legibles y por tanto explicables. Datos de negocio: desistimiento 14 días; garantía legal 3 años (1095 días) para producto nuevo; higiene y precintados no devolvibles si se abren; etiqueta original; envío de devolución gratuito si es defectuoso.
7.1 Base de conocimiento, reglas y memoria de trabajo
import operator
OPERADORES = {"==": operator.eq, "!=": operator.ne, "<=": operator.le, "<": operator.lt,
">=": operator.ge, ">": operator.gt, "in": lambda a, b: a in b}
class Regla:
"""Regla SI condiciones ENTONCES conclusiones, con prioridad y texto de justificación."""
def __init__(self, nombre, condiciones, conclusiones, prioridad=0, justificacion=""):
self.nombre = nombre
self.condiciones = condiciones # lista de (atributo, operador, valor)
self.conclusiones = conclusiones # dict atributo -> valor
self.prioridad = prioridad
self.justificacion = justificacion
def evaluar(self, memoria):
"""True si todas las condiciones se cumplen, False si alguna falla,
None si falta algún hecho para decidir."""
resultado = True
for atributo, op, valor in self.condiciones:
if atributo not in memoria.hechos:
resultado = None # aún no sabemos: seguimos por si otra falla
continue
if not OPERADORES[op](memoria.hechos[atributo], valor):
return False
return resultado
def hechos_que_faltan(self, memoria):
return [a for a, _, _ in self.condiciones if a not in memoria.hechos]
def __repr__(self):
cond = " ∧ ".join(f"{a} {op} {v!r}" for a, op, v in self.condiciones)
return f"{self.nombre} [prio {self.prioridad}]: SI {cond} ENTONCES {self.conclusiones}"
class BaseConocimiento:
def __init__(self):
self.reglas = []
def agregar(self, regla):
self.reglas.append(regla)
def reglas_que_concluyen(self, atributo):
return [r for r in self.reglas if atributo in r.conclusiones]
class MemoriaTrabajo:
"""Hechos del caso actual + registro de quién los puso y cuándo."""
def __init__(self, hechos_iniciales=None):
self.hechos = {}
self.origen = {} # atributo -> "entrada" | "usuario" | nombre de regla
self.reloj = 0
self.tiempo = {} # atributo -> instante en que se afirmó (para recencia)
for k, v in (hechos_iniciales or {}).items():
self.afirmar(k, v, "entrada")
def afirmar(self, atributo, valor, origen):
self.reloj += 1
self.hechos[atributo] = valor
self.origen[atributo] = origen
self.tiempo[atributo] = self.reloj
def recencia(self, regla):
return max((self.tiempo.get(a, 0) for a, _, _ in regla.condiciones), default=0)Explicación: Regla.evaluar distingue tres resultados, no dos: verdadero, falso y "no se sabe" (None) cuando falta un hecho; ese tercer valor es lo que permitirá a la interfaz preguntar exactamente lo que falta. MemoriaTrabajo guarda, además del valor de cada hecho, su origen (dato de entrada, respuesta del usuario o regla que lo dedujo) y el instante en que se afirmó: el origen alimenta la explicación y el instante, la recencia.
7.2 Motor de inferencia con resolución de conflictos y explicación
class MotorInferencia:
def __init__(self, base, memoria):
self.base = base
self.memoria = memoria
self.disparadas = [] # nombres de reglas ya aplicadas (refractoriedad)
self.traza = [] # explicación paso a paso
# ---- encadenamiento hacia delante con resolución de conflictos ----
def conjunto_conflicto(self):
return [r for r in self.base.reglas
if r.nombre not in self.disparadas and r.evaluar(self.memoria) is True]
def elegir(self, candidatas):
"""Prioridad > especificidad (nº de condiciones) > recencia de los hechos usados."""
return max(candidatas, key=lambda r: (r.prioridad, len(r.condiciones),
self.memoria.recencia(r)))
def disparar(self, regla):
detalle = ", ".join(f"{a}={self.memoria.hechos[a]!r} ({op} {v!r})"
for a, op, v in regla.condiciones)
for atributo, valor in regla.conclusiones.items():
self.memoria.afirmar(atributo, valor, regla.nombre)
self.disparadas.append(regla.nombre)
self.traza.append(f"Se aplicó {regla.nombre} porque {detalle} "
f"→ {regla.conclusiones}. Justificación: {regla.justificacion}")
def ejecutar(self, objetivo="decision"):
while objetivo not in self.memoria.hechos:
candidatas = self.conjunto_conflicto()
if not candidatas:
self.traza.append(f"Ninguna regla aplicable y '{objetivo}' sin fijar")
return None
if len(candidatas) > 1:
self.traza.append("Conflicto entre " + ", ".join(r.nombre for r in candidatas)
+ f"; gana {self.elegir(candidatas).nombre}")
self.disparar(self.elegir(candidatas))
return self.memoria.hechos[objetivo]
# ---- módulo de explicación ----
def por_que(self, atributo):
origen = self.memoria.origen.get(atributo)
if origen is None:
return f"'{atributo}' no está en la memoria de trabajo"
if origen in ("entrada", "usuario"):
return f"'{atributo}' = {self.memoria.hechos[atributo]!r} es un dato de {origen}"
regla = next(r for r in self.base.reglas if r.nombre == origen)
return (f"'{atributo}' = {self.memoria.hechos[atributo]!r} lo fijó {regla.nombre} "
f"({regla.justificacion}); sus condiciones: "
+ "; ".join(self.por_que(a) for a, _, _ in regla.condiciones))
def como(self):
return "\n".join(f" {i+1}. {paso}" for i, paso in enumerate(self.traza))
# ---- interfaz de consulta dirigida por objetivo (hacia atrás simplificado) ----
def consultar(self, objetivo, preguntas, responder):
"""Recorre las reglas que concluyen el objetivo por prioridad; si a una le
faltan hechos preguntables, los pide; dispara la primera que se cumple."""
for regla in sorted(self.base.reglas_que_concluyen(objetivo),
key=lambda r: (-r.prioridad, -len(r.condiciones))):
for atributo in regla.hechos_que_faltan(self.memoria):
if regla.evaluar(self.memoria) is False:
break # ya sabemos que no aplica: no preguntar más
if atributo in preguntas:
valor = responder(preguntas[atributo], regla)
self.memoria.afirmar(atributo, valor, "usuario")
if regla.evaluar(self.memoria) is True:
self.disparar(regla)
return self.memoria.hechos[objetivo]
return NoneExplicación:
conjunto_conflictoes el paso "emparejar" del ciclo: reglas no disparadas aún (refractoriedad) cuyas condiciones son todas ciertas.elegires la resolución de conflictos en una línea:maxcon una clave compuesta (prioridad, número de condiciones, recencia), que aplica los criterios en ese orden.dispararafirma las conclusiones en la memoria (con la regla como origen) y escribe en la traza una frase completa con los valores reales que satisfacían cada condición: la materia prima del "¿cómo?".ejecutarrepite el ciclo hasta fijar el objetivo (decision) o quedarse sin reglas. Cuando hay más de una candidata, lo deja anotado en la traza: veremos "Conflicto entre R3, R4; gana R3".por_quesigue la cadena de orígenes: si el hecho lo fijó una regla, explica la regla y, recursivamente, cada una de sus condiciones.comodevuelve la traza numerada.consultares el encadenamiento hacia atrás simplificado dirigido por objetivo: toma las reglas que concluyendecisionordenadas como las ordenaría el motor (prioridad, especificidad), y para cada una pregunta solo los hechos que le faltan, deteniéndose en cuanto una condición ya conocida falla (para no preguntar cosas inútiles). La primera regla que resulta cierta se dispara.responder(pregunta, regla)es una función que se le pasa: en producción leerá del teclado o de un formulario; en la lección la simularemos para poder ejecutar sin teclado, y le pasamos la regla para que pueda contestar "¿por qué?".
7.3 Las reglas de devoluciones y garantías
def base_devoluciones():
kb = BaseConocimiento()
kb.agregar(Regla("R1", [("defectuoso", "==", True), ("producto_nuevo", "==", True),
("dias_desde_entrega", "<=", 1095)],
{"decision": "garantia", "envio_devolucion": "gratuito"}, prioridad=10,
justificacion="garantía legal de 3 años del producto nuevo; el envío lo paga NovaMarket"))
kb.agregar(Regla("R2", [("defectuoso", "==", True), ("dias_desde_entrega", ">", 1095)],
{"decision": "denegada", "motivo": "fuera del plazo de garantía legal"}, prioridad=9,
justificacion="pasados 3 años se ofrece servicio técnico de pago"))
kb.agregar(Regla("R3", [("categoria", "in", ("higiene", "precintado")), ("abierto", "==", True)],
{"decision": "denegada", "motivo": "producto de higiene o precintado abierto"}, prioridad=8,
justificacion="excepción al desistimiento: precintados e higiene no se devuelven abiertos"))
kb.agregar(Regla("R4", [("dias_desde_entrega", "<=", 14), ("usado", "==", False),
("etiqueta_original", "==", True)],
{"decision": "reembolso_desistimiento", "envio_devolucion": "a cargo del cliente"}, prioridad=5,
justificacion="derecho de desistimiento de 14 días con producto sin usar y etiquetado"))
kb.agregar(Regla("R5", [("dias_desde_entrega", "<=", 14), ("usado", "==", False),
("etiqueta_original", "==", True), ("devoluciones_previas", ">=", 5)],
{"decision": "revision_humana", "motivo": "patrón de devoluciones frecuentes"}, prioridad=5,
justificacion="más específica que R4: el caso 3 (fraude) pide una mirada humana"))
kb.agregar(Regla("R6", [("dias_desde_entrega", "<=", 14), ("usado", "==", True)],
{"decision": "revision_humana", "motivo": "producto usado dentro de plazo: valorar depreciación"},
prioridad=4, justificacion="una persona decide si procede reembolso total o parcial"))
kb.agregar(Regla("R7", [("dias_desde_entrega", "<=", 14), ("etiqueta_original", "==", False)],
{"decision": "revision_humana", "motivo": "falta la etiqueta original"}, prioridad=4,
justificacion="la política exige etiqueta; una persona valora la excepción"))
kb.agregar(Regla("R8", [("dias_desde_entrega", ">", 14), ("defectuoso", "==", False)],
{"decision": "denegada", "motivo": "fuera del plazo de desistimiento y sin defecto"}, prioridad=2,
justificacion="la regla de Diego: usado o no, pasados 14 días sin defecto no procede"))
return kbLee las prioridades como la jerarquía de la política: primero la garantía (R1, R2: un defecto se atiende con independencia del desistimiento, resolviendo el choque de 06-01), luego las excepciones al desistimiento (R3), luego el desistimiento ordinario (R4) y sus variantes que piden revisión humana (R5-R7, en línea con el human-in-the-loop de 02-04: la denegación o el reembolso parcial de un producto usado no se automatizan), y por último la regla de Diego (R8). Nota que R5 y R4 tienen la misma prioridad: la decidirá la especificidad.
7.4 Casos de prueba
CASOS = {
"A cafetera NovaBrew": dict(dias_desde_entrega=9, usado=False, etiqueta_original=True, defectuoso=False, producto_nuevo=True, categoria="general", abierto=True, devoluciones_previas=0),
"B aspirador NovaClean": dict(dias_desde_entrega=40, usado=True, etiqueta_original=False, defectuoso=True, producto_nuevo=True, categoria="general", abierto=True, devoluciones_previas=1),
"C cepillo NovaSmile": dict(dias_desde_entrega=5, usado=False, etiqueta_original=True, defectuoso=False, producto_nuevo=True, categoria="higiene", abierto=True, devoluciones_previas=0),
"D auriculares NovaSound": dict(dias_desde_entrega=20, usado=True, etiqueta_original=True, defectuoso=False, producto_nuevo=True, categoria="general", abierto=True, devoluciones_previas=2),
"E monitor NovaView": dict(dias_desde_entrega=3, usado=False, etiqueta_original=True, defectuoso=False, producto_nuevo=True, categoria="general", abierto=False, devoluciones_previas=7),
}
for nombre, datos in CASOS.items():
motor = MotorInferencia(base_devoluciones(), MemoriaTrabajo(datos))
decision = motor.ejecutar("decision")
print(f"{nombre}: decision = {decision}, {motor.memoria.hechos.get('envio_devolucion') or motor.memoria.hechos.get('motivo')}")
print(motor.como())
motor = MotorInferencia(base_devoluciones(), MemoriaTrabajo(CASOS["C cepillo NovaSmile"]))
motor.ejecutar("decision")
print("¿Por qué decision?\n ", motor.por_que("decision"))Salida:
A cafetera NovaBrew: decision = reembolso_desistimiento, a cargo del cliente
1. Se aplicó R4 porque dias_desde_entrega=9 (<= 14), usado=False (== False), etiqueta_original=True (== True) → {'decision': 'reembolso_desistimiento', 'envio_devolucion': 'a cargo del cliente'}. Justificación: derecho de desistimiento de 14 días con producto sin usar y etiquetado
B aspirador NovaClean: decision = garantia, gratuito
1. Se aplicó R1 porque defectuoso=True (== True), producto_nuevo=True (== True), dias_desde_entrega=40 (<= 1095) → {'decision': 'garantia', 'envio_devolucion': 'gratuito'}. Justificación: garantía legal de 3 años del producto nuevo; el envío lo paga NovaMarket
C cepillo NovaSmile: decision = denegada, producto de higiene o precintado abierto
1. Conflicto entre R3, R4; gana R3
2. Se aplicó R3 porque categoria='higiene' (in ('higiene', 'precintado')), abierto=True (== True) → {'decision': 'denegada', 'motivo': 'producto de higiene o precintado abierto'}. Justificación: excepción al desistimiento: precintados e higiene no se devuelven abiertos
D auriculares NovaSound: decision = denegada, fuera del plazo de desistimiento y sin defecto
1. Se aplicó R8 porque dias_desde_entrega=20 (> 14), defectuoso=False (== False) → {'decision': 'denegada', 'motivo': 'fuera del plazo de desistimiento y sin defecto'}. Justificación: la regla de Diego: usado o no, pasados 14 días sin defecto no procede
E monitor NovaView: decision = revision_humana, patrón de devoluciones frecuentes
1. Conflicto entre R4, R5; gana R5
2. Se aplicó R5 porque dias_desde_entrega=3 (<= 14), usado=False (== False), etiqueta_original=True (== True), devoluciones_previas=7 (>= 5) → {'decision': 'revision_humana', 'motivo': 'patrón de devoluciones frecuentes'}. Justificación: más específica que R4: el caso 3 (fraude) pide una mirada humana
¿Por qué decision?
'decision' = 'denegada' lo fijó R3 (excepción al desistimiento: precintados e higiene no se devuelven abiertos); sus condiciones: 'categoria' = 'higiene' es un dato de entrada; 'abierto' = True es un dato de entradaLos cinco casos cubren la política: A, desistimiento limpio (el cliente paga el envío de vuelta); B, el aspirador defectuoso a los 40 días, usado y sin etiqueta, va por garantía con envío gratuito: R1 gana a todo lo demás, y el choque de 06-01 ha desaparecido por prioridad; C, el cepillo de higiene abierto: R3 y R4 son ambas aplicables (conflicto) y gana R3 por prioridad, con lo que la incoherencia del ejercicio 3 de 06-01 queda resuelta y documentada en la traza; D, la regla de Diego; E, mismo caso que A pero con 7 devoluciones previas: R4 y R5 empatan en prioridad y gana R5 por especificidad, derivando a revisión humana. La llamada a por_que muestra la explicación encadenada hasta los datos de entrada.
7.5 Interfaz de preguntas: consulta dirigida por el objetivo
PREGUNTAS = {
"defectuoso": "¿El producto presenta un defecto de fabricación o funcionamiento? (s/n)",
"producto_nuevo": "¿Se vendió como producto nuevo (no reacondicionado)? (s/n)",
"dias_desde_entrega": "¿Cuántos días han pasado desde la entrega?",
"categoria": "Categoría del producto (general/higiene/precintado)",
"abierto": "¿Está abierto o desprecintado? (s/n)",
"usado": "¿Muestra señales de uso? (s/n)",
"etiqueta_original": "¿Conserva la etiqueta original? (s/n)",
"devoluciones_previas": "¿Cuántas devoluciones previas tiene el cliente?",
}
def convertir(texto):
t = texto.strip().lower()
if t in ("s", "si", "sí"): return True
if t in ("n", "no"): return False
try: return int(t)
except ValueError: return t
def responder_simulado(respuestas):
"""Devuelve una función 'responder' que lee de un dict en vez de input()
(para poder ejecutar la sesión sin teclado). Con '?' explica por qué pregunta."""
def responder(pregunta, regla):
atributo = next(k for k, v in PREGUNTAS.items() if v == pregunta)
print(f" Sistema: {pregunta}")
if respuestas.get(atributo + "?"):
print(f" Usuario: ¿por qué?\n Sistema: porque intento aplicar {regla.nombre}: {regla}")
texto = respuestas[atributo]
print(f" Usuario: {texto}")
return convertir(texto)
return responder
# Portátil NovaBook devuelto con datos parciales: el sistema pregunta lo que necesita
sesion = {"defectuoso": "n", "dias_desde_entrega": "10", "categoria": "general", "usado": "s", "usado?": True}
motor = MotorInferencia(base_devoluciones(), MemoriaTrabajo({}))
decision = motor.consultar("decision", PREGUNTAS, responder_simulado(sesion))
print(f" Decisión: {decision} ({motor.memoria.hechos.get('motivo', '')})")
print(" ¿Cómo?\n" + motor.como())Salida:
Sistema: ¿El producto presenta un defecto de fabricación o funcionamiento? (s/n)
Usuario: n
Sistema: Categoría del producto (general/higiene/precintado)
Usuario: general
Sistema: ¿Cuántos días han pasado desde la entrega?
Usuario: 10
Sistema: ¿Muestra señales de uso? (s/n)
Usuario: ¿por qué?
Sistema: porque intento aplicar R5: R5 [prio 5]: SI dias_desde_entrega <= 14 ∧ usado == False ∧ etiqueta_original == True ∧ devoluciones_previas >= 5 ENTONCES {'decision': 'revision_humana', 'motivo': 'patrón de devoluciones frecuentes'}
Usuario: s
Decisión: revision_humana (producto usado dentro de plazo: valorar depreciación)
¿Cómo?
1. Se aplicó R6 porque dias_desde_entrega=10 (<= 14), usado=True (== True) → {'decision': 'revision_humana', 'motivo': 'producto usado dentro de plazo: valorar depreciación'}. Justificación: una persona decide si procede reembolso total o parcialSigue el hilo: el sistema empieza por la regla de mayor prioridad (R1) y pregunta si es defectuoso; con "no", R1 y R2 quedan descartadas sin más preguntas. Pasa a R3 y pregunta la categoría; "general" la descarta, y no pregunta si está abierto (la condición ya conocida falla). Pasa a R5 (misma prioridad que R4 pero más específica): pregunta los días y si está usado; el usuario pide "¿por qué?" y el módulo de explicación responde con la regla que está intentando; "sí, usado" descarta R5 y R4, y R6 se cumple: revisión humana por producto usado dentro de plazo. En total 4 preguntas de 8 posibles: el encadenamiento dirigido por objetivo pregunta solo lo relevante, como MYCIN. Para usarlo con teclado basta pasar responder=lambda pregunta, regla: convertir(input(pregunta + " ")).
- Herramientas reales
No hace falta escribir el motor cada vez. En producción se usan:
- CLIPS (NASA, años 80, en C): el clásico shell de reglas con encadenamiento hacia delante y el algoritmo Rete, que empareja reglas y hechos de forma eficiente cuando hay miles de ambos; sintaxis tipo Lisp.
- Drools (Java, código abierto): motor de reglas de negocio (BRMS) muy usado en banca y seguros, con tablas de decisión y el estándar DMN que veremos en 06-04.
experta(Python, heredera de PyKnow): reglas como métodos decorados con@Rule, hechos como objetosFact, motor con salience (prioridad). No está en el entorno de este curso, pero nuestro código traduce casi línea a línea a sus conceptos:Regla↔@Rule,MemoriaTrabajo↔declare,prioridad↔salience.
Con lo aprendido aquí, la documentación de cualquiera de ellas te resultará familiar; 07-03 las sitúa en el ecosistema.
Errores Comunes y Consejos
- Reglas sin prioridad ni orden pensado. Es el error de 06-01: dos reglas aplicables y el resultado depende del orden de escritura. Decide explícitamente la jerarquía (garantía > excepciones > desistimiento > regla general) y escríbela en la justificación.
- Reglas que "saben" demasiado. Una regla que mezcla ocho condiciones es ilegible y frágil. Descompón en hechos intermedios (
en_garantia,dentro_plazo) como hicimos en 06-01, y en reglas de dos o tres condiciones. - Atributos ambiguos. ¿
abiertoes "desprecintado" o "usado"? Diego distingue; el diccionario de datos debe distinguir. Los sistemas expertos mueren de vocabulario impreciso antes que de lógica. - Olvidar el caso "ninguna regla aplicable". Nuestro motor devuelve
None; en producción eso debe traducirse en revisión humana, no en denegación por defecto (mundo cerrado, 06-01) ni en un error. - No mantener la batería de casos. Cada regla nueva puede cambiar decisiones antiguas. Los casos A-E son el conjunto de test; añade cada caso real conflictivo que aparezca.
- Confiar la política a la máquina. El sistema aplica reglas; el responsable de las reglas es Diego, y de la normativa, un profesional legal. Que la traza diga siempre qué regla y qué versión decidió.
- Automatizar lo que exige una persona. Reembolsos parciales, denegaciones dudosas y patrones de fraude van a
revision_humana; es diseño, no cobardía (02-04).
Ejercicios
Ejercicio 1. Diego recuerda una excepción: durante la campaña de Navidad el plazo de desistimiento se amplía a 30 días para los pedidos entregados en diciembre. Añade a la memoria de trabajo un atributo campana_navidad (True/False) y escribe la regla o reglas necesarias sin romper las existentes. ¿Qué prioridad les das y por qué? Comprueba con el caso D (auriculares, 20 días) con campana_navidad=True y con False.
Ejercicio 2. Ejecuta consultar con la sesión {"defectuoso": "s", "producto_nuevo": "s", "dias_desde_entrega": "400"} y anota cuántas preguntas hace y qué decide. Después cambia dias_desde_entrega a "1200". Explica, mirando el orden de las reglas en consultar, por qué en ambos casos el sistema no pregunta nada sobre categoría, uso ni etiqueta.
Ejercicio 3. Diseña, sin código, la sesión de adquisición del conocimiento con Diego para añadir al sistema las garantías comerciales ampliadas que NovaMarket vende con algunos productos (por ejemplo, 2 años adicionales para portátiles). Indica: (a) tres preguntas que le harías, (b) qué atributos nuevos necesitaría la memoria de trabajo, (c) qué casos de prueba añadirías a la batería A-E y (d) qué podría salir mal en la interacción con R1 y R2.
Soluciones
Solución 1. Una forma limpia: R4n, prioridad 5, condiciones campana_navidad == True, dias_desde_entrega <= 30, usado == False, etiqueta_original == True → reembolso_desistimiento; y para que la regla de Diego (R8) no deniegue entre los días 15 y 30 en campaña, o bien se le añade la condición campana_navidad == False (y una R8n con campana_navidad == True y dias_desde_entrega > 30), o bien, más simple, se sube R4n por encima de R8, cosa que ya ocurre (prioridad 5 frente a 2), porque el motor para al fijar decision. Con campana_navidad=True el caso D (usado) sigue sin entrar en R4n (exige sin usar) y necesita una R6n análoga a R6 con 30 días (revisión humana); con False, R8 deniega como antes. La lección: cada excepción temporal toca varias reglas; conviene agruparlas y probarlas juntas.
Solución 2. Con 400 días: pregunta defectuoso (sí), producto_nuevo (sí) y dias_desde_entrega (400): tres preguntas, R1 se cumple → garantia, envío gratuito. Con 1200 días: las mismas tres preguntas; R1 falla en la tercera condición, pasa a R2 (defectuoso == True, dias > 1095), que ya tiene todos los hechos y se cumple → denegada, "fuera del plazo de garantía legal". consultar recorre las reglas por prioridad descendente y dispara la primera que se cumple: como R1 y R2 (prioridades 10 y 9) se resuelven con esos tres hechos, nunca llega a R3-R8, cuyas preguntas (categoría, uso, etiqueta) son irrelevantes para un producto defectuoso.
Solución 3. (a) "¿Qué productos llevan garantía ampliada y cómo se sabe en el pedido?"; "¿la ampliada cubre lo mismo que la legal (defectos) o también accidentes?"; "¿quién paga el envío y quién repara, NovaMarket o el fabricante?". (b) garantia_ampliada_meses (o fin_garantia_ampliada), tipo_cobertura, quizá causa_defecto (fabricación/accidente). (c) Portátil NovaBook defectuoso a los 4 años con ampliada de 2 años → garantía (ampliada); el mismo sin ampliada → denegada por R2; un daño accidental con ampliada que no lo cubre → denegada o revisión humana. (d) R2 deniega a partir de 1095 días antes de considerar la ampliada: habría que darle a la nueva regla prioridad entre R1 y R2 (por ejemplo 9,5) o añadir a R2 la condición de que no haya ampliada vigente; y hay que aclarar en el diccionario que producto_nuevo y garantia_ampliada son atributos distintos.
Conclusión
En esta lección hemos convertido el motor de 06-01 en un sistema experto. Hemos visto qué es (conocimiento explícito separado del razonamiento) y de dónde viene (DENDRAL, MYCIN, XCON), y hemos recorrido su arquitectura: base de conocimiento, memoria de trabajo, motor de inferencia con resolución de conflictos (refractoriedad, prioridad, especificidad, recencia), módulo de explicación (¿por qué? y ¿cómo?), adquisición del conocimiento e interfaz. Hemos descrito el proceso de ingeniería del conocimiento (entrevistas a Diego, extracción y formalización de reglas, validación con casos, mantenimiento con propietario), y hemos comparado ventajas e inconvenientes y el sistema experto con un modelo de ML: reglas escritas frente a reglas aprendidas, con el árbol de 04-04 como puente y la combinación de ambos como destino. En código, Regla, BaseConocimiento, MemoriaTrabajo y MotorInferencia han decidido cinco casos de devoluciones y garantías de NovaMarket con traza de explicación (la garantía del NovaClean por prioridad, el cepillo NovaSmile por prioridad sobre el desistimiento, el monitor NovaView por especificidad), y consultar ha preguntado solo lo necesario, con "¿por qué?" incluido.
Pero fíjate en cómo hemos tratado la incertidumbre: no la hemos tratado. defectuoso es verdadero o falso; revision_humana es la salida de escape cuando no queremos decidir. En las devoluciones eso es aceptable, porque la política es nítida. En el diagnóstico de incidencias del caso 9 no lo es: un paquete abollado sugiere daño en transporte pero también un error de picking con mal embalaje; un retraso apunta a Getafe pero también a la mensajería; y incidencias.csv tiene la mitad de las causas sin rellenar. Ahí las reglas nítidas fallan y hace falta razonar con grados de creencia: los factores de certeza que inventó MYCIN, la lógica difusa y, sobre todo, la probabilidad, el teorema de Bayes y las redes bayesianas que relanzaron la IA en los 90 (01-01). Es el tema de la próxima lección, 06-03.
Fundamentos de Inteligencia Artificial (IA)
Módulo 1: Introducción a la Inteligencia Artificial
Módulo 2: Principios Básicos de la IA
- Conceptos Fundamentales: Agentes, Entornos y Racionalidad
- Tipos de Inteligencia Artificial
- Los Datos como Materia Prima de la IA
- Ética y Consideraciones en IA
Módulo 3: Algoritmos en IA
- Introducción a los Algoritmos
- Algoritmos de Búsqueda
- Búsqueda con Adversario: Juegos y Minimax
- Algoritmos de Optimización
Módulo 4: Aprendizaje Automático (Machine Learning)
- Conceptos Básicos de Machine Learning
- Tipos de Aprendizaje Automático
- Preparación de Datos y Características
- Algoritmos de Machine Learning
- Evaluación y Validación de Modelos
- Sobreajuste, Regularización y Ajuste de Hiperparámetros
Módulo 5: Redes Neuronales y Deep Learning
- Introducción a las Redes Neuronales
- Arquitectura de Redes Neuronales
- Cómo Aprende una Red: Descenso del Gradiente y Retropropagación
- Deep Learning y sus Aplicaciones
- Transformers, Grandes Modelos de Lenguaje e IA Generativa
Módulo 6: Lógica y Sistemas Expertos
- Lógica en IA
- Sistemas Expertos
- Razonamiento con Incertidumbre: Probabilidad y Redes Bayesianas
- Aplicaciones de Sistemas Expertos
Módulo 7: Herramientas y Lenguajes de Programación en IA
- Lenguajes de Programación para IA
- Python Científico: NumPy, pandas y Matplotlib
- Herramientas y Librerías Populares
- Entornos de Desarrollo
Módulo 8: Proyectos y Casos de Estudio
Módulo 9: Ejercicios y Prácticas
- Ejercicios de Algoritmos
- Prácticas de Machine Learning
- Proyectos de Redes Neuronales
- Proyecto Integrador: de la Idea al Prototipo
