Perguntas e respostas
Uma pergunta do Laya é uma decisão tipada, e o tipo decide tanto o que você pergunta quanto o que recebe de volta. Existem três, e um mesmo estado pode carregar todas elas em uma única passada 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 única passada direta responde às três. Esse é o sentido 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 um
formato 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 rótulo para descrição. A ordem é posicional na pergunta
renderizada, então duas perguntas com os mesmos rótulos em ordem diferente são perguntas diferentes.
A descrição é opcional; {"billing": None} renderiza apenas o rótulo. Os rótulos são devolvidos
exatamente como você os escreveu, então um rótulo não textual volta como ele mesmo em choice e
como a chave em probabilities.
Vale a pena escrever a descrição. Não é decoração: o texto renderizado da pergunta é o que o modelo
lê, então um {"a": None, "b": None} seco não dá a ele 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 caminhos, e sua resposta é a probabilidade de verdadeiro, 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 é você quem escolhe, porque depende do que um falso positivo custa a você. Não há nenhum
campo bool para confundir com uma decisão.
Você pode renomear 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 não muda: noul continua sendo P(true).
{"type": "noul", "instructions": "Does this need a human?",
"labels": {"false": "automatic", "true": "escalate"}}
Os rótulos não são uma forma de consertar uma pergunta que o modelo erra. O noul segue seus próprios
rótulos de opção, com mais força no checkpoint em inglês, então um par de rótulos que lê como uma
decisão (“approve” / “reject”) pode puxar a resposta para o rótulo em vez do estado. Valide qualquer
renomeação nos seus 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 ela deve 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, score é 2.90 enquanto o único
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, e o argmax minimiza a discordância com o
modelo. Se você quer o rótulo, tome o argmax de probabilities ou leia a contraparte de
answer_confidence na legenda — não arredonde score e suponha que seja o rótulo. legend existe
para você nunca precisar adivinhar qual índice significa o quê.
Ler a confiança
Toda resposta carrega dois números de confiança, e eles medem coisas diferentes.
| campo | o que é | aplicar gating sobre ele? |
|---|---|---|
answer_confidence |
max(p) — a probabilidade da resposta que está sendo reportada |
sim, após o 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 quantidade que o escalonamento por temperatura ajusta e sobre a qual cada
número de calibração do repositório é calculado, o que é o que faz dele o número sobre o qual aplicar
gating. Ele não está calibrado como distribuído: a propriedade que costuma ser atribuída a ele —
a de que das respostas retornadas com confiança c, cerca de c estão certas — só vale depois que
as temperaturas foram ajustadas e validadas em dados reservados para o seu checkpoint com o seu
número de opções. Os checkpoints distribuídos são excessivamente confiantes e o quanto depende do
número de opções, então um limiar sem ajuste 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 é a 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, então os dois não devem ser comparados contra um único 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 eles são iguais por construção — sobre duas opções, max(p, 1-p) é max(p) — então uma
resposta noul não pode dizer qual dos dois você esteve lendo. As duas chaves estão presentes em
todos os tipos para que a escolha seja explícita em vez de implícita.
Um limiar é uma política, não uma propriedade do modelo. Os dois checkpoints são distribuídos
excessivamente confiantes, e o quanto depende do número de opções, então um número medido em uma
pergunta de 3 opções não se transfere para uma de 20. Meça-o nos seus próprios dados; a seção de
Calibração do
BENCHMARKS.md tem o ciclo de ajuste
e os valores ajustados.
action e act_probability
action.act_probability é a pontuação de uma cabeça separada para “um agente deveria agir sobre isto
afinal?”, distinta da confiança da própria resposta. Ela é reportada para todos os tipos de pergunta.
Nada na biblioteca aplica um limiar sobre ela por você.
Presets
Três conjuntos de perguntas prontos, para os casos comuns não precisarem de critérios escritos à mão:
from laya import triage_questions, guard_questions, moderation_questions
agent.system_one(ticket, triage_questions())
Use-os como ponto de partida, não como contrato — leia as perguntas que eles produzem com
render_options e verifique se os rótulos cabem no seu domínio antes de colocá-los em produção.
Ler as opções de volta
Como a ordem das opções é posicional e o texto da opção é 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']
Os mesmos rótulos em ordem diferente renderizam nessa ordem, que é por 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']
Repare nas chaves. render_options aceita a forma interna de chaves curtas
{"t": ..., "crit": ...}, não a forma {"type": ..., "criteria": ...} que você escreve em uma
pergunta — passar a forma pública lança KeyError: 't'.
A conversão é um mapeamento curto e estável que você pode embutir, o que evita recorrer a um helper
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 rótulos; um
dicionário criteria e uma lista score passam sem mudança.
Limites que vale conhecer antes de projetar em cima disto
- Confiança não é garantia de correção em contagens altas de opções. Em uma pergunta de 20 opções as distribuições de respostas certas e erradas se sobrepõem muito, e um limiar pode acabar selecionando abaixo da própria precisão do modelo. Veja #394.
- A negação não é tratada de forma confiável em perguntas de escolha forçada. Uma pergunta de cancelamento pode retornar o rótulo de cancelamento para um estado que diz não cancelar, com alta confiança, nos dois checkpoints. Veja #377.
noulpode seguir seus rótulos em vez do estado, então valide qualquer renomeação.- Mais opções não é de graça. Depois de cerca de 20, o modelo degrada rápido; use o helper de pré-seleção para reduzir um espaço grande de rótulos antes de perguntar, ou divida-o em uma pergunta grossa e uma fina.