Preguntas y respuestas
Una pregunta de Laya es una decisión tipada, y el tipo decide tanto lo que preguntas como lo que recibes. Hay tres, y un mismo estado puede llevarlas todas en una sola pasada hacia adelante:
import laya
agent = laya.load("convaiinnovations/laya")
questions = {
"dept": {"type": "choice", "instructions": "Which team should handle this?",
"criteria": {"billing": "money and invoices",
"technical": "bugs and outages",
"sales": "pricing and contracts"}},
"urgent": {"type": "noul", "instructions": "Is this urgent?"},
"severity": {"type": "score", "instructions": "How severe is this?",
"criteria": ["trivial", "minor", "moderate", "serious", "critical"]},
}
result = agent.system_one({"text": "I was charged twice and nobody has replied for a week. "
"Please refund me."}, questions)
result["answers"]["dept"]["choice"] # 'billing'
result["answers"]["urgent"]["noul"] # 0.8727
result["answers"]["severity"]["score"] # 2.9046
Una sola pasada hacia adelante responde a las tres. Ese es el sentido de la interfaz tipada: una pregunta
noul no es un choice con dos opciones que resultan ser “sí” y “no” — es una cabeza distinta con una
forma de salida distinta, y el tipo le dice a Laya cuál usar.
Los tres tipos
choice — elegir una de un conjunto
{"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {"billing": "money and invoices", "technical": "bugs and outages"}}
criteria es un mapeo ordenado de etiqueta a descripción. El orden es posicional en la pregunta
renderizada, así que dos preguntas con las mismas etiquetas en distinto orden son preguntas distintas.
Una descripción es opcional; {"billing": None} renderiza solo la etiqueta. Las etiquetas se devuelven
exactamente como las escribiste, así que una etiqueta no textual vuelve como sí misma en choice y como
la clave en probabilities.
Vale la pena escribir la descripción. No es decoración: el texto renderizado de la pregunta es lo que lee
el modelo, así que un {"a": None, "b": None} escueto no le da nada para distinguir las opciones.
{"type": "choice", "choice": "billing",
"probabilities": {"billing": 0.9881, "technical": 0.0057, "sales": 0.0062},
"confidence": 0.9339, "answer_confidence": 0.9881,
"action": {"act_probability": 1.0}}
noul — sí o no
{"type": "noul", "instructions": "Is this urgent?"}
noul es el nombre del proyecto para una
decisión binaria, y su respuesta es la probabilidad de verdadero, no un booleano con umbral:
{"type": "noul", "noul": 0.8727, "confidence": 0.8727, "answer_confidence": 0.8727,
"action": {"act_probability": 1.0}}
El umbral lo eliges tú, porque depende de lo que te cueste un falso positivo. No hay ningún campo bool
que se pueda confundir con una decisión.
Puedes reetiquetar las dos opciones cuando la decisión no sea naturalmente un sí/no — labels toma
exactamente las claves false y true, y la polaridad no cambia: noul sigue siendo P(true).
{"type": "noul", "instructions": "Does this need a human?",
"labels": {"false": "automatic", "true": "escalate"}}
Las etiquetas no son una forma de arreglar una pregunta que el modelo responde mal. noul sigue sus
propias etiquetas de opción, con más fuerza en el checkpoint inglés, así que un par de etiquetas que se
lea como una decisión (“approve” / “reject”) puede arrastrar la respuesta hacia la etiqueta en lugar de
hacia el estado. Valida cualquier reetiquetado con tus propios datos antes de confiar en él.
score — un nivel ordenado
{"type": "score", "instructions": "How severe is this?",
"criteria": ["trivial", "minor", "moderate", "serious", "critical"]}
criteria es una lista ordenada, y debe estar ordenada de forma ascendente — la posición es la escala.
{"type": "score", "score": 2.9046,
"legend": {"0": "trivial", "1": "minor", "2": "moderate", "3": "serious", "4": "critical"},
"probabilities": {"0": 0.0134, "1": 0.05, "2": 0.0518, "3": 0.7881, "4": 0.0967},
"confidence": 0.5187, "answer_confidence": 0.7881,
"action": {"act_probability": 1.0}}
score es un valor esperado, no el nivel más probable. Arriba, score es 2.90 mientras que el único
nivel más probable es serious (3) con 0.788. Ambos son útiles y responden a preguntas distintas: el
valor esperado minimiza el error cuadrático sobre la escala, y el argmax minimiza la discrepancia con el
modelo. Si quieres la etiqueta, toma el argmax de probabilities o lee la contraparte de
answer_confidence en la leyenda — no redondees score y asumas que es la etiqueta. legend existe
para que nunca tengas que adivinar qué significa cada índice.
Leer la confianza
Cada respuesta lleva dos números de confianza, y miden cosas distintas.
| campo | qué es | ¿aplicar gating sobre él? |
|---|---|---|
answer_confidence |
max(p) — la probabilidad de la respuesta que se informa |
sí, tras el ajuste |
confidence |
1 - H(p) / log(k) — cuán concentrada está toda la distribución |
no |
probabilities |
la distribución completa (choice, score) |
— |
answer_confidence es la cantidad que ajusta el escalado por temperatura y sobre la que se calcula cada
cifra de calibración del repositorio, que es lo que la convierte en la cantidad sobre la que aplicar
gating. No está calibrada tal como se distribuye: la propiedad que se le suele atribuir — que de las
respuestas devueltas con confianza c, aproximadamente c son correctas — solo se sostiene una vez que
las temperaturas se han ajustado y validado sobre datos reservados para tu checkpoint con tu número de
opciones. Los checkpoints publicados son demasiado confiados y cuánto depende del número de opciones,
así que un umbral sin ajustar puede seleccionar por debajo de la propia precisión del modelo
(#394).
# THRESHOLD is a number you measured on your own held-out data, not one the model ships.
# Fit and validate the temperatures first — the fine-tuning notebook has the loop:
# notebooks/laya_finetune_typed_decisions_2xT4_kaggle.ipynb
ans = result["answers"]["dept"]
if ans["answer_confidence"] >= THRESHOLD:
...
confidence es la entropía normalizada: alta cuando la distribución está concentrada, baja cuando está
repartida, sin importar si la respuesta principal es correcta. Es una señal útil y no está en la
misma escala, así que las dos no deben compararse contra un solo número:
# the same three answers, and the two numbers are not the same
dept confidence 0.9339 answer_confidence 0.9881
urgent confidence 0.8727 answer_confidence 0.8727
severity confidence 0.5187 answer_confidence 0.7881
Para noul son iguales por construcción — sobre dos opciones, max(p, 1-p) es max(p) — así que una
respuesta noul no puede decirte cuál de las dos has estado leyendo. Ambas claves están presentes en
todos los tipos para que la elección sea explícita en lugar de implícita.
Un umbral es una política, no una propiedad del modelo. Ambos checkpoints se distribuyen demasiado
confiados, y cuánto depende del número de opciones, así que un número medido en una pregunta de 3
opciones no se traslada a una de 20. Mídelo con tus propios datos; la sección de Calibración de
BENCHMARKS.md tiene el bucle de ajuste
y los valores ajustados.
action y act_probability
action.act_probability es la puntuación de una cabeza aparte para “¿debería un agente actuar sobre esto
en absoluto?”, distinta de la confianza de la propia respuesta. Se informa para todos los tipos de
pregunta. Nada en la biblioteca le aplica un umbral por ti.
Presets
Tres conjuntos de preguntas ya listos, para que los casos comunes no necesiten criterios escritos a mano:
from laya import triage_questions, guard_questions, moderation_questions
agent.system_one(ticket, triage_questions())
Úsalos como punto de partida más que como contrato — lee las preguntas que producen con render_options
y comprueba que las etiquetas encajan en tu dominio antes de ponerlos en producción.
Volver a leer las opciones
Como el orden de las opciones es posicional y el texto de las opciones es lo que lee el modelo, vale la pena poder ver exactamente lo que se envió:
from laya import render_options
render_options({"t": "choice", "crit": {"billing": None, "sales": "pricing"}})
# ['billing', 'sales: pricing']
render_options({"t": "score", "crit": ["low", "high"]})
# ['level 0: low', 'level 1: high']
Las mismas etiquetas en distinto orden se renderizan en ese orden, que es por lo que el orden forma parte de la identidad de la pregunta:
render_options({"t": "choice", "crit": {"x": "first", "y": "second"}})
# ['x: first', 'y: second']
render_options({"t": "choice", "crit": {"y": "second", "x": "first"}})
# ['y: second', 'x: first']
Fíjate en las claves. render_options toma la forma interna de claves cortas {"t": ..., "crit": ...},
no la forma {"type": ..., "criteria": ...} que escribes en una pregunta — pasar la forma pública lanza
KeyError: 't'.
La conversión es un mapeo corto y estable que puedes integrar, lo que evita recurrir a un ayudante
privado — Agent._to_internal es interno y puede cambiar:
def as_internal(q):
"""The short-key shape `render_options` reads, from a question as you wrote it."""
crit = q.get("criteria")
if q["type"] == "choice" and isinstance(crit, list):
crit = {c: None for c in crit}
return {"t": q["type"], "ins": q["instructions"], "crit": crit}
render_options(as_internal(question))
Eso refleja lo que hace la biblioteca para una pregunta choice escrita como lista de etiquetas; un
diccionario criteria y una lista score pasan sin cambios.
Límites que conviene conocer antes de diseñar sobre esto
- La confianza no es una garantía de corrección con muchas opciones. En una pregunta de 20 opciones las distribuciones de las respuestas correctas e incorrectas se solapan mucho, y un umbral puede acabar seleccionando por debajo de la propia precisión del modelo. Consulta #394.
- La negación no se maneja de forma fiable en preguntas de opción forzada. Una pregunta de cancelación puede devolver la etiqueta de cancelación para un estado que dice no cancelar, con alta confianza, en ambos checkpoints. Consulta #377.
noulpuede seguir sus etiquetas en lugar del estado, así que valida cualquier reetiquetado.- Más opciones no es gratis. Pasadas unas 20, el modelo se degrada rápido; usa el ayudante de preselección para reducir un espacio grande de etiquetas antes de preguntar, o divídelo en una pregunta gruesa y otra fina.