Perguntas e respostas
Uma pergunta do Laya é uma decisão tipada, e o tipo decide tanto o que perguntas como o que recebes de volta. Há três, e um estado pode transportar todas numa única passagem direta:
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
Uma passagem direta responde a todas as três. É esse o ponto da interface tipada: uma pergunta
noul não é um choice com duas opções que por acaso são «sim» e «não» — é uma cabeça diferente
com uma forma de saída diferente, e o tipo diz ao Laya qual usar.
Os três tipos
choice — escolher um de um conjunto
{"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {"billing": "money and invoices", "technical": "bugs and outages"}}
criteria é um mapeamento ordenado de etiqueta para descrição. A ordem é posicional na pergunta
renderizada, por isso duas perguntas com as mesmas etiquetas numa ordem diferente são perguntas
diferentes. A descrição é opcional; {"billing": None} renderiza apenas a etiqueta. As etiquetas
são devolvidas exatamente como as escreveste, por isso uma etiqueta que não seja string volta como
ela própria em choice e como a chave em probabilities.
Vale a pena escrever a descrição. Não é decoração: o texto da pergunta renderizado é o que o modelo
lê, por isso um {"a": None, "b": None} nu não lhe dá nada para distinguir as opções.
{"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 — sim ou não
{"type": "noul", "instructions": "Is this urgent?"}
noul é o nome do projeto para uma
decisão de dois sentidos, e a sua resposta é a probabilidade de verdadeiro, e não um booleano
com limiar:
{"type": "noul", "noul": 0.8727, "confidence": 0.8727, "answer_confidence": 0.8727,
"action": {"act_probability": 1.0}}
O limiar é teu para escolher, porque depende do que te custa um falso positivo. Não há nenhum campo
bool que possas confundir com uma decisão.
Podes reetiquetar as duas opções quando a decisão não é naturalmente um sim/não — labels aceita
exatamente as chaves false e true, e a polaridade fica inalterada: noul continua a ser P(true).
{"type": "noul", "instructions": "Does this need a human?",
"labels": {"false": "automatic", "true": "escalate"}}
As etiquetas não são uma forma de corrigir uma pergunta que o modelo erra. O noul segue as suas
próprias etiquetas de opção, mais fortemente no checkpoint inglês, por isso um par de etiquetas que
se lê como uma decisão («approve» / «reject») pode puxar a resposta para a etiqueta em vez do
estado. Valida qualquer reetiquetagem nos teus próprios dados antes de confiar nela.
score — um nível ordenado
{"type": "score", "instructions": "How severe is this?",
"criteria": ["trivial", "minor", "moderate", "serious", "critical"]}
criteria é uma lista ordenada, e tem de estar em ordem crescente — a posição é a 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 é um valor esperado, não o nível mais provável. Acima, o score é 2.90 enquanto o nível
mais provável é serious (3) com 0.788. Ambos são úteis e respondem a perguntas diferentes: o valor
esperado minimiza o erro quadrático sobre a escala, o argmax minimiza a discordância com o modelo.
Se queres a etiqueta, toma o argmax de probabilities ou lê a contraparte de answer_confidence na
legend — não arredondes o score e assumas que é a etiqueta. A legend existe para que nunca
tenhas de adivinhar que índice significa o quê.
Ler a confiança
Cada resposta transporta dois números de confiança, e medem coisas diferentes.
| campo | o que é | fazer gating sobre ele? |
|---|---|---|
answer_confidence |
max(p) — a probabilidade da resposta que está a ser reportada |
sim, após ajuste |
confidence |
1 - H(p) / log(k) — quão concentrada está a distribuição inteira |
não |
probabilities |
a distribuição completa (choice, score) |
— |
answer_confidence é a grandeza que a escala de temperatura ajusta e sobre a qual é calculada cada
cifra de calibração no repositório, e é isso que faz dele aquele sobre o qual fazer gating. Não
está calibrado tal como é distribuído: a propriedade que normalmente lhe é atribuída — a de que,
das respostas devolvidas com confiança c, cerca de c estão corretas — só é válida depois de as
temperaturas terem sido ajustadas e validadas em dados reservados para o teu checkpoint com a tua
contagem de opções. Os checkpoints distribuídos são demasiado confiantes e o quanto depende da
contagem de opções, por isso um limiar não afinado pode selecionar abaixo da própria precisão do
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 é entropia normalizada: alta quando a distribuição está concentrada, baixa quando está
espalhada, independentemente de a resposta principal estar correta. É um sinal útil e não está
na mesma escala, por isso os dois não devem ser sujeitos a gating contra um só 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 são iguais por construção — sobre duas opções, max(p, 1-p) é max(p) — por isso uma
resposta noul não te consegue dizer qual dos dois estiveste a ler. Ambas as chaves estão presentes
em todos os tipos, para que a escolha seja explícita e não implícita.
Um limiar é uma política, não uma propriedade do modelo. Ambos os checkpoints são distribuídos
demasiado confiantes, e o quanto demasiado confiantes depende da contagem de opções, por isso um
número medido numa pergunta de 3 opções não se transfere para uma de 20. Mede-o nos teus próprios
dados; a secção de Calibração do
BENCHMARKS.md tem o ciclo de
ajuste e os valores ajustados.
action e act_probability
action.act_probability é o score de uma cabeça separada para «um agente deve agir sobre isto de
todo», distinto da confiança da própria resposta. É reportado para todos os tipos de pergunta. Nada
na biblioteca lhe aplica um limiar por ti.
Presets
Três conjuntos de perguntas prontos a usar, para que os casos comuns não precisem de criteria escritos à mão:
from laya import triage_questions, guard_questions, moderation_questions
agent.system_one(ticket, triage_questions())
Usa-os como ponto de partida e não como contrato — lê as perguntas que produzem com
render_options e verifica se as etiquetas se ajustam ao teu domínio antes de os pôr em produção.
Ler as opções de volta
Como a ordem das opções é posicional e o texto das opções é o que o modelo lê, vale a pena conseguir ver exatamente o que foi enviado:
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']
As mesmas etiquetas numa ordem diferente renderizam nessa ordem, e é por isso que a ordem faz parte da identidade da pergunta:
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']
Atenção às chaves. O render_options aceita a forma interna de chaves curtas {"t": ..., "crit": ...}, e não a forma {"type": ..., "criteria": ...} que escreves numa pergunta — passar a
forma pública levanta KeyError: 't'.
A conversão é um mapeamento curto e estável que podes inserir diretamente, o que evita ir buscar um
ajudante privado — Agent._to_internal é interno e pode mudar:
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))
Isso espelha o que a biblioteca faz para uma pergunta choice escrita como lista de etiquetas; um
dict criteria e uma lista score passam inalterados.
Limites que vale a pena conhecer antes de desenhares em torno disto
- A confiança não é uma garantia de correção em contagens elevadas de opções. Numa pergunta de 20 opções as distribuições para respostas certas e erradas sobrepõem-se muito, e um limiar pode acabar por selecionar abaixo da própria precisão do modelo. Vê #394.
- A negação não é tratada de forma fiável em perguntas de escolha forçada. Uma pergunta de cancelamento pode devolver a etiqueta de cancelamento para um estado que diz para não cancelar, com confiança elevada, em ambos os checkpoints. Vê #377.
- O
noulpode seguir as suas etiquetas em vez do estado, por isso valida qualquer reetiquetagem. - Mais opções não é grátis. Para lá de cerca de 20 o modelo degrada-se depressa; usa o ajudante de pré-seleção para reduzir um espaço grande de etiquetas antes de perguntar, ou divide-o numa pergunta grosseira e numa fina.