Documentação

Harness de avaliação

laya.evals transforma um conjunto de dados rotulado em uma pontuação repetível, e uma linha de base em um gate de passa/falha, para que uma mudança de qualidade seja um diff revisável em vez de uma checagem manual.

A matemática das métricas e o parser do conjunto de dados são Python puro mais numpy e nunca importam o torch, então rodam sem pesos. Rodar um conjunto de dados contra um checkpoint precisa do checkpoint e leva seu tempo normal de carga.

Início rápido

# check the format without a model
laya-evals validate research/evals/fixture.jsonl

# score a labelled set on one checkpoint, with thresholds and a baseline
laya-evals run data.jsonl --model english --device cpu \
    --min-accuracy 0.8 --max-ece 0.05 --score-within 0.25 --slice language \
    --json report.json --markdown report.md

# compare a saved report to a baseline
laya-evals compare report.json --baseline baseline.json --tolerance choice_accuracy=0.02

laya eval ... é a mesma coisa através da CLI principal, então laya eval validate data.jsonl também funciona.

Códigos de saída: 0 em sucesso, 1 quando um limiar ou uma tolerância de linha de base falha, 2 em erro de uso. run imprime as métricas gerais e quaisquer fatias pedidas na saída padrão, e grava o relatório completo e um resumo em Markdown quando --json / --markdown são informados.

Atribuir erros de shortlist

Para um conjunto de escolhas rotulado de alta cardinalidade, laya.evals_shortlist.evaluate_shortlist usa o caminho predict_shortlist existente e o harness de avaliação regular. Ele responde a duas perguntas separadas: a recuperação manteve o rótulo ouro, e o Laya o escolheu quando ele estava presente? Esta é uma API Python opt-in para rótulos de choice; relatórios comuns de laya-evals run não mudam.

import laya
from laya.evals import Dataset
from laya.evals_shortlist import evaluate_shortlist
from laya.shortlist import embed_fn_from_agent

agent = laya.load()
dataset_path = "intents.jsonl"
dataset = Dataset.from_jsonl(dataset_path)
report = evaluate_shortlist(
    agent, dataset, embed_fn_from_agent(agent), k=20,
    checkpoint_id="my-checkpoint@revision", embedder_id="my-encoder@revision",
    dataset_path=dataset_path,
)
print(report.overall)
print(report.cases[0]["shortlist_status"])

Use a mesma função de embedding e o mesmo checkpoint da implantação que está sendo medida. Os dois identificadores são fornecidos pelo chamador e devem nomear revisões imutáveis; o relatório não consegue inferir os pesos por trás de um callable arbitrário. dataset_path registra o SHA256 do arquivo ao lado da impressão digital de perguntas existente. Cada caso mantém os rótulos de shortlist reais e um dentre correct, retrieval_miss ou decision_miss. shortlist_recall_at_k é a fração de rótulos ouro retidos. shortlist_accuracy_on_recalled são as decisões corretas divididas pelos casos retidos; é omitido quando nenhum foi retido. O choice_accuracy existente continua sendo a acurácia de ponta a ponta sobre todos os casos, incluindo os erros de recuperação. As métricas de shortlist aparecem nas mesmas fatias de idioma, modelo, pergunta e tag. A latência da solicitação inclui o embedding e a chamada de decisão; o relatório não isola os tempos de cada etapa. Com k >= n, a pergunta original passa direto e a recuperação tem recall 1 sem chamar o embedder.

Isto não reproduz os resultados do BANKING77 na issue #102: esses números dependem do conjunto de dados, do checkpoint e do bi-encoder dela. Esta API torna o mesmo tipo de diagnóstico repetível em um conjunto rotulado próprio do chamador.

Avaliar uma exportação ONNX

run --onnx PATH pontua um modelo ONNX exportado através do ONNXAgent em vez do Router do torch, então uma implantação ONNX (incluindo uma cópia INT8 de scripts/export_onnx.py --quantize) passa pelos mesmos limiares e linhas de base que o caminho do torch:

python scripts/export_onnx.py --model convaiinnovations/laya --output laya.onnx --quantize
laya-evals run data.jsonl --onnx laya.int8.onnx --max-ece 0.05

--model nomeia o checkpoint de onde veio a exportação — um id do Hub ou caminho local, não um nome curto de Router como english, já que não há Router nesse caminho (padrão convaiinnovations/laya). Sua config e seu tokenizer são carregados de lá. O agente serve um único checkpoint, então uma linha do conjunto de dados cujo campo model nomeia outro falha com um erro claro em vez de ser respondida em silêncio pelo modelo errado; --device não se aplica. --batch-size usa a API de lote do agente quando ele tem uma e cai para uma chamada por estado caso contrário; --sort-by-length é repassado para essa API de lote; o fallback por estado não tem grupo para reordenar. Passe --calibration PATH para carregar um mapa de calibração ajustado no ONNXAgent, de modo que gates de calibração como --max-ece avaliem contra probabilidades calibradas. O bloco config do relatório registra o caminho onnx e o caminho calibration (quando definido).

Medido em research/evals/fixture.jsonl (12 linhas rotuladas, checkpoint em inglês, CPU):

runner choice_acc noul_acc score_mae ece mean_conf p50 ms
torch Router 0.75 1.00 1.3418 0.1596 0.7304 116.8
--onnx fp32 0.75 1.00 1.3418 0.1596 0.7304 66.3
--onnx int8 0.75 1.00 1.3512 0.1658 0.7304 46.3

A exportação fp32 reproduz os números do torch exatamente, e a cópia quantizada move score_mae em 0.009 e ece em 0.006 — o tipo de desvio que compare --tolerance deve barrar.

Formato do conjunto de dados

Um objeto JSON por linha (JSONL). Linhas em branco e linhas que começam com # são ignoradas.

campo obrigatório significado
state sim texto, e-mail, ticket ou documento JSON sobre o qual decidir
questions sim um dict de pergunta do Laya, exatamente como Router.predict aceita
expected sim verdade de referência indexada por id de pergunta: um rótulo para choice, um número para score, true/false para noul
tags não strings pelas quais fatiar
language não um código pelo qual fatiar
model não força um checkpoint para esta linha; --model o sobrescreve. Uma linha que não força nada é rotulada com o checkpoint com que o Router respondeu

research/evals/dataset.template.jsonl tem um exemplo comentado.

Métricas

Cada métrica é calculada por resposta onde se aplica e agregada sobre o conjunto de dados:

métrica aplica-se a significado
choice_accuracy choice fração cujo rótulo escolhido corresponde
noul_accuracy noul fração cujo booleano (probabilidade >= 0.5) corresponde
score_mae score erro absoluto médio
score_within_<tol> score fração dentro de uma tolerância absoluta
ece qualquer resposta com confiança erro de calibração esperado, 15 bins, calculado sobre answer["answer_confidence"], a probabilidade calibrada que o Laya reporta em todo tipo de resposta
brier qualquer resposta com confiança e rótulo conhecido Brier score da confiança como P(correct), mean((confidence - correct)**2); menor é melhor
aurc qualquer resposta com confiança e rótulo conhecido área sob a curva risco–cobertura: um valor de risco por nível de confiança distinto, cada um ponderado pelas respostas que aquele nível abrange; menor é melhor, e recompensa uma confiança que ordena certo e errado, não apenas calibrada
selective_accuracy@50, selective_accuracy@80 qualquer resposta com confiança e rótulo conhecido acurácia sobre as respostas que um limiar de confiança no ponto de cobertura de 50% / 80% aceita – o que abster-se da cauda menos confiante compra. Um limiar não consegue dividir um grupo de confianças iguais, então isso pode cobrir mais do que a fração nomeada; veja cortes de cobertura
mean_confidence qualquer resposta com confiança média do answer["answer_confidence"] reportado
latency_p50_ms, latency_p95_ms por solicitação tempo de parede que cada solicitação esperou, informativo – veja batching
cost_per_decision_p50_ms, cost_per_decision_p95_ms por decisão tempo de parede de uma chamada dividido pelas linhas que ela carregou, informativo

Cortes de cobertura e empates

Ambas as métricas de cobertura cortam em um limiar de confiança, e um limiar aceita toda resposta em sua própria confiança. Então um corte nunca divide um grupo de respostas que compartilham uma: quando coverage * n cai dentro de tal grupo, todo membro do grupo é aceito. O número de respostas por trás da cifra é, portanto, a borda superior do grupo, e não a fração nomeada – selective_accuracy@50 sobre uma fatia cujas confianças são todas iguais é a própria acurácia dessa fatia, não a melhor metade dela. A contagem que o gate imprime (n= na mensagem de falha de uma regra) é o tamanho da fatia, não o tamanho aceito, então um grupo muito largo não é visível apenas pela mensagem.

Empates são o caso normal, não um canto: uma temperatura ajustada pode deixar um bucket reportando uma massa pontual, o que laya.common.answer_confidence registra do choice:11+ distribuído, e um checkpoint real produziu um grupo de seis linhas exatamente em 1.0 de doze respostas. Cortar em um índice de linha em vez disso fez ambas as métricas dependerem da ordem em que o conjunto de dados chegou – as mesmas linhas, embaralhadas, moveram selective_accuracy@50 entre 0.000 e 1.000.

aurc integra um valor de risco por nível distinto, ponderado pelas respostas que aquele nível abrange, então continua sendo uma área sob a curva risco–cobertura, e não uma média de pontos de tamanhos desiguais.

Duas consequências para as quais vale planejar:

  • Um número pode se mover em qualquer direção, por mais do que uma reordenação poderia. Onde um grupo atravessa o corte, a leitura por limiar difere de toda leitura por índice de linha dos mesmos dados: medido em 400.001 conjuntos de dados em forma de empate, até 0.500 para selective_accuracy@50 e 0.351 para aurc. Na forma de doze respostas acima – seis corretas, todas com confiança 1.0 – aurc se move 0.327 (0.173 a 0.500). Um gate que estava passando pode falhar, e um que estava falhando pode passar; o veredito anterior dependia da ordem das linhas, inclusive para um limite min ou max absoluto, que nada recusa porque lê uma única execução.
  • Regenere as linhas de base commitadas. config.coverage_metric_definition registra qual definição produziu um relatório. Comparar uma métrica de cobertura entre duas definições é recusado em ambos os gates que subtraem uma linha de base – --baseline --tolerance (EvalReport.compare) e uma regra relativa (max_drop / max_increase) sob --gate-policy – e em qualquer dos lados estar desatualizado, não só a linha de base: um candidato produzido por um laya mais antigo carrega um artefato de ordem de linha que pode ler melhor que a verdade, então barrá-lo contra uma linha de base regenerada corretamente passaria uma regressão que o relatório pontuado corretamente reprova. Sem essa recusa, uma linha de base desatualizada esconde uma regressão real: uma fatia registrada em 0.033 sob a definição antiga lê 0.517 sob esta, então um candidato que caiu genuinamente 0.217 passaria por um max_drop de 0.05. ece e brier não cortam e continuam comparáveis.

Sem empates nos dados há um nível por resposta, e ambas as métricas são exatamente o que sempre foram – idênticas bit a bit, não apenas próximas.

Adicione ScoreWithin(0.25) à lista de avaliadores para uma métrica de tolerância; o conjunto padrão é choice_accuracy, noul_accuracy, score_mae, mean_confidence, mais ece. Pela CLI a mesma coisa é uma flag: laya-evals run data.jsonl --score-within 0.25 reporta score_within_0.25 ao lado dos padrões, e a flag se repete, então --score-within 0.25 --score-within 0.5 reporta os dois.

Uma métrica de tolerância precisa de uma resposta score com rótulo numérico, então em um conjunto de dados sem uma ela não tem valor: run nomeia a métrica que não conseguiu calcular em vez de publicar um zero silencioso, e um gate --min / --max que nomeie essa métrica falha como ausente. As tolerâncias que uma execução pediu são registradas no bloco config do relatório, então uma linha de base revisada diz quais colunas ela espera.

Batching e medição de tempo

--batch-size N pontua até N linhas consecutivas que compartilham um checkpoint e um esquema de perguntas em uma única chamada. As duas métricas de tempo vêm das mesmas medições e respondem a perguntas diferentes: toda linha de um lote retorna quando o lote retorna, então sua latency é a chamada inteira, enquanto seu cost_per_decision é 1/N dela. O batching, portanto, aumenta latency_* e reduz cost_per_decision_* em um conjunto inalterado de decisões, e --max latency_p50_ms=... pergunta se as solicitações foram atendidas rápido, não se a execução foi barata. Sem --batch-size os dois concordam.

compare ignora qualquer métrica *_ms a menos que uma tolerância a nomeie, então essas nunca falham uma linha de base por ruído de tempo. O que o harness de fato fez — o tamanho de lote pedido, o formato de runner em que ele se resolveu, quantas linhas compartilharam uma chamada e o maior pedaço — é registrado em config.timing do relatório, porque a flag sozinha não diz se algo foi agrupado em lote. Esses contadores registram as chamadas emitidas, não as chamadas que retornaram: com laya-evals run --on-error skip, um pedaço cuja chamada lançou ainda conta em rows_grouped e max_chunk, ao lado de suas entradas em config.errored. O padrão é --on-error fail, que relança em vez de publicar um relatório cujas métricas cobrem apenas as chamadas que voltaram. As duas métricas *_ms contam apenas as chamadas que retornaram, então uma chamada falha nunca contribui com uma latência que ela não mediu.

Agrupar as linhas dentro de um lote

--sort-by-length agrupa linhas de tamanhos semelhantes na mesma passada direta, então cada passada preenche até um máximo mais curto em vez de até a linha mais longa que há nela. É o formato das chamadas, não as suas respostas: os resultados voltam na mesma ordem e pontuam de forma idêntica, que é por que research/ consegue reportar 2.15x sobre 10.000 tickets sem nenhuma decisão mudar.

Precisa haver mais de uma passada para reordenar, então só tem efeito com um --batch-size N abaixo do número de linhas que a execução agrupa. config.timing mantém as duas afirmações separadas: sort_by_length é o que a linha de comando disse, sort_by_length_sent é o que chegou ao runner. Uma execução sem --batch-size pede algo que não pode acontecer, e diz isso com sent: false; um runner cujo predict_batch é anterior ao ajuste é pontuado sem ordenação em vez de lançar TypeError no meio de uma execução longa.

O gate de abstenção em um limiar

--min-confidence T repassa o limiar de abstenção opt-in do core (#361) para toda chamada que a execução faz, então o Router e o ONNXAgent marcam respostas cujo answer_confidence fica abaixo de T com low_confidence: True antes de o harness vê-las. Diferente do agrupamento, isso muda as respostas que pontuam: a mesma execução com T=0 e T=0.7 é um experimento diferente, e uma varredura de precision@coverage é uma série destes, não uma única linha de base à deriva.

O intervalo aceito é o laya.confidence.check_min_confidence do core – [0.0, 1.0], finito, não um bool – em vez de uma cópia aqui, então um valor que o próprio gate recusaria falha como erro de uso (saída 2) antes de qualquer checkpoint carregar. 0.0 é um pedido legal: é o braço de controle de uma varredura de precision@coverage, e uma verificação que o descartasse esconderia o próprio piso da varredura.

Um runner cujo predict ou (para uma execução em lote) cujo predict_batch é anterior ao gate é recusado com um EvalError nomeado, não pontuado sem o limiar. Descartar silenciosamente um controle de pontuação é a classe de mentira que este harness existe para prevenir: o relatório publicaria um número de precision@coverage para uma política que nunca rodou. config.timing registra tanto o pedido quanto o fato: min_confidence é o limiar que foi solicitado, min_confidence_sent diz se alguma chamada desta execução de fato o carregou.

Fatias

compare e run reportam os números gerais e, para --slice language|model|qid|tag, as mesmas métricas por valor de fatia, então uma regressão em um idioma ou em uma pergunta fica visível sem ler o agregado. A fatia model guarda o checkpoint que respondeu cada linha: a escolha do próprio Router por solicitação, ou o model do runner para um runner que não roteia.

Gates de fatia opt-in

O gate de linha de base geral pode passar enquanto uma fatia menor de idioma ou pergunta regride. Para tornar uma fatia revisada um requisito de CI, salve uma política JSON como gates.json:

{
  "version": 1,
  "rules": [
    {"slice": {"language": "zh"}, "metric": "choice_accuracy",
     "min_count": 50, "max_drop": 0.05},
    {"slice": {"qid": "intent"}, "metric": "ece",
     "min_count": 50, "max": 0.10}
  ]
}
laya-evals run data.jsonl --baseline baseline.json --tolerance choice_accuracy=0.02 \
    --gate-policy gates.json --json report.json
laya-evals compare report.json --baseline baseline.json \
    --tolerance choice_accuracy=0.02 --gate-policy gates.json

Cada regra seleciona exatamente um valor de language, model, qid ou tag e nomeia a métrica exatamente como ela aparece no relatório de fatia. Ela tem um min_count positivo e exatamente um limite: min ou max verifica o valor candidato; max_drop permite no máximo aquela diminuição em relação à linha de base; max_increase permite no máximo aquele aumento. Os dois últimos exigem --baseline. A contagem é o número de respostas pontuadas para aquela métrica na fatia selecionada, em ambos os relatórios para uma regra relativa. Para ece, é o número de respostas com confiança finita e valor correct booleano. Uma fatia ou métrica ausente, poucas respostas pontuadas, ou casos pulados/com erro reprovam o gate opt-in. Regras relativas também exigem que ambos os relatórios carreguem identidades de execução correspondentes, para que evidência ausente não possa aparecer como um passe. Uma regressão medida reporta a fatia, a métrica, as contagens, os valores e o limite. Sintaxe de política inválida sai com 2 antes de um checkpoint carregar; uma falha de qualidade sai com 1. A política é registrada em config.gate_policy de um relatório de run --json. compare --gate-policy aplica a política fornecida naquela linha de comando às medições salvas. Se ela difere da política registrada no relatório, o compare diz isso; uma reverificação explícita sob uma nova política não muda a política sob a qual a execução original foi feita.

A comparação geral regular ainda se aplica, incluindo sua tolerância e o comportamento de linha de base legada. Sem --gate-policy, o relato e a comparação de fatias se comportam como antes.

Identidade da execução

run registra o que mediu no bloco config do relatório, então o artefato que um revisor lê é revisável por si só:

chave significado
schema o formato do relatório, laya-evals-report/1, para um consumidor poder recusar um que não consegue ler
dataset o caminho como digitado – um nome, não um hash
dataset_sha256 o sha256 dos bytes do conjunto de dados que foram analisados
questions_sha256 uma impressão digital do esquema de perguntas: id, tipo, instructions e criteria de cada pergunta, sobre todo o conjunto de dados
laya_version o laya que calculou os números
coverage_metric_definition qual definição de aurc / selective_accuracy@* produziu este relatório (veja cortes de cobertura). Uma regra de gate relativa sobre qualquer um deles recusa uma linha de base registrada sob uma definição diferente, em vez de subtrair números que não significam a mesma coisa
thresholds o gate que esta execução aplicou: min, max e baseline_tolerance
gate_policy a política opcional de gate de fatia aplicada por run --gate-policy
revisions o commit de onde cada checkpoint que respondeu foi carregado (veja abaixo)

dataset é um caminho, e um caminho não é uma identidade: um conjunto de dados pode ser editado no lugar, movido ou baixado de novo sob o mesmo nome, e um cache de CI pode entregar a duas execuções o mesmo nome de arquivo e bytes diferentes. questions_sha256 cobre o que foi perguntado em vez de quantas linhas havia, então adicionar estados a um conjunto de perguntas inalterado deixa a impressão digital intacta — dataset_sha256 ainda se move, e adicionar uma linha é uma mudança nos dados, não na pergunta.

Ele cobre instructions também, porque o texto da instrução é o prompt. build_sequence renderiza "<type> question: <instructions>" na cabeça tokenizada, o Agent recusa uma pergunta sem uma (“adicione o texto que o modelo deve responder”), e a própria identidade de pergunta do Laya já a conta: Router._question_schema e o agrupamento em lote deste harness usam como chave o dict de perguntas inteiro, e tests/test_router_batch.py fixa que reformular apenas instructions move uma linha para o seu próprio grupo de lote. Uma instrução reformulada ainda se compara igual a uma linha de base? Não — e esse é o ponto. “Julgue se um reembolso é justificado” e “Seja conservador e só aprove pedidos de reembolso explícitos” fazem perguntas diferentes, e o gate de métrica só consegue notar quando a diferença por acaso move um número além da tolerância que você nomeou. Nomear uma opção de choice é o mesmo argumento: criteria é o espaço de decisão, e os checks metamórficos em research/eval/metamorphic.py existem porque renomear um rótulo vira as respostas.

Nada do texto da instrução é normalizado exceto o único passo que o próprio motor aplica: uma instructions não textual é transformada em hash como json.dumps(ins, ensure_ascii=False), correspondendo a Agent._to_internal. Então espaços em branco e redação contam, e uma reformulação que um humano considera uma edição de cópia é tratada como um novo experimento. Esse é o padrão honesto — a alternativa é uma heurística de similaridade entre uma execução e sua linha de base, e nenhum sistema de avaliação de uso comum tem uma.

Nada que carregue tempo é registrado, então um relatório continua reproduzível byte a byte para um runner fixo.

REPORT_SCHEMA, questions_fingerprint(dataset) e file_fingerprint(path) são públicos, então um chamador que usa laya.evals.evaluate diretamente obtém a mesma identidade que uma execução da CLI.

Linha de base e gate de CI

  • Mantenha o conjunto de dados, um relatório de linha de base (saída --json que você revisou) e as tolerâncias juntos, com commit, para que uma mudança seja um diff revisável. --tolerance METRIC=VALUE é o desvio absoluto máximo permitido para aquela métrica.
  • laya-evals run ... --baseline baseline.json --tolerance ... sai com código diferente de zero em caso de desvio, então cai direto em CI sem mudança. laya.evals.EvalReport.compare e assert_regression expõem a mesma lógica para testes.

O gate de métrica responde “os números se moveram”. Ele não consegue responder “eram os mesmos números”, porque compare lê overall e apenas overall — então uma linha de base registrada contra um conjunto de dados passaria por um candidato pontuado em outro, com aritmética idêntica. EvalReport.comparable_to fecha isso: ele compara schema, dataset_sha256 e questions_sha256, e run --baseline e compare ainda imprimem todo delta, então falham com saída diferente de zero nomeando a chave e os dois valores:

FAIL: baseline is not comparable: dataset_sha256 (dataset bytes): baseline is <sha>, this run is <sha>

Uma chave ausente em qualquer dos lados é desconhecida, não um conflito, então todo relatório escrito antes de a identidade existir continua comparando exatamente como antes. Isso inclui a linha de base do gate agendado abaixo, que vem de research/eval/ e não tem nenhum config.schema.

Duas superfícies de CI usam isto:

  • um job sem pesos em .github/workflows/ci.yml roda tests/test_evals.py e tests/test_evals_api.py, então a matemática das métricas, o parsing do conjunto de dados e a CLI ficam cobertos em todo PR sem baixar um checkpoint;
  • .github/workflows/evals.yml roda semanalmente, antes de uma versão e sob demanda: ele avalia o checkpoint em inglês na suíte MASSIVE em inglês e compara com research/results/eval_english_51_languages.json com as tolerâncias em research/evals/thresholds.json. Ele sobe o relatório como artefato e não bloqueia um PR.

O harness é determinístico para uma revisão de checkpoint fixa, então um relatório é reproduzível. run registra o conjunto de dados, o modelo e o dispositivo, mais os fatos de tempo da execução, no bloco config do relatório, e revisions: o commit de onde cada checkpoint que respondeu foi de fato carregado. --revision <SHA> fixa esse commit para todo checkpoint que a execução carrega, e --revision english=<SHA> fixa um checkpoint (repetível) — que é a forma que uma execução com roteamento automático quer, já que os três checkpoints são três repositórios e um commit não pode existir em todos eles. Deixado sem fixar, a execução pega o branch padrão do checkpoint e o relatório ainda diz qual commit respondeu, então um desvio de linha de base pode ser atribuído aos pesos ou ao código. laya/revisions.py publica SHAs de commit revisados em PINNED_REVISIONS para chamadores que querem optar. Com --onnx, apenas um --revision <SHA> puro se aplica, ao download da config e do tokenizer.

Adicionar o conjunto rotulado real

Coloque um JSONL em research/evals/ e uma linha de base revisada ao lado dele, depois aponte um fluxo de trabalho (ou research/evals/check_regression.py) para os dois. O formato é o mesmo do fixture; nada no harness sabe de MASSIVE.