Documentação

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 noul pode 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.