Guias
Tutorial: avaliar uma política de aprendizado por reforço
Um passo a passo executável para dois tipos de pergunta de aprendizado por reforço: quão bem uma política que você pode executar se sai ao longo de episódios, e quão bem uma política que você não pode testar no mundo teria se saído, julgada a partir do log da política que ela substituiria. Os dois exemplos rodam localmente, sem credenciais de provedor.
A referência de cada campo e avaliador está em Aprendizado por reforço; os termos caso, avaliador, métrica, intervalo e gate estão em Conceitos básicos. Esta página é o caminho prático por eles.
O que o Oloproof faz e não faz aqui
O Oloproof não executa o seu ambiente, não hospeda um simulador nem treina uma política. O seu sistema executa cada episódio no próprio ambiente e o registra como um artefato rl_episode/v1, e toda métrica de episódio é lida desse registro. Na segunda parte, nada é executado: a política em teste só informa a probabilidade que daria a cada ação.
Mantenha as duas perguntas separadas:
| Pergunta | O que você tem | Parte |
|---|---|---|
| Quão bem esta política se sai ao longo de episódios? | Uma política e um ambiente que você pode executar | 1 |
| Quão bem esta política teria se saído em produção? | Um log de decisões que uma política mais antiga tomou, com as probabilidades que ela usou | 2 |
Pré-requisitos
- Python 3.11 ou posterior, e o Oloproof instalado (pip install oloproof).
- O projeto de exemplo, que vem com o pacote. Copie-o para um novo diretório:
oloproof init --example rl my-rlEle contém dois projetos, episodes/ e off_policy/. Todos os comandos abaixo rodam de dentro de um deles, e a evidência é armazenada no seu diretório .oloproof/.
Parte 1: uma política que você pode executar
cd my-rl/episodesOs arquivos
| Arquivo | O que é |
|---|---|
| env.py | Um mundo em grade, duas políticas (wander e plan) e os sistemas que as executam |
| data/tasks.jsonl | 24 grades, cada uma uma tarefa: paredes, perigos, um início e um objetivo |
| oloproof.yaml | A suíte para a política de linha de base, wander |
| candidate.yaml | A mesma suíte para a política candidata, plan |
| release.yaml | A política de lançamento |
| comparison.yaml | As regras que uma candidata precisa cumprir em relação à linha de base |
Um episódio avança uma célula por passo até alcançar o objetivo ou esgotar os passos. Cada passo rende -1 e alcançar o objetivo rende +10, então um retorno fica entre -30 (30 passos, sem objetivo) e 10. Entrar em um perigo custa 1: essa é a restrição de segurança. Um movimento em cada dez escorrega em uma direção aleatória, seja qual for a escolha da política, então a mesma política pode se sair de forma diferente na mesma grade.
Registrando um episódio
O próprio sistema executa o episódio e entrega o registro ao gravador do caso:
@system(name="grid-agent", version="wander-v1", records=("rl_episode/v1",))
def run_baseline(task):
return _run(task, wander)
def _run(task, policy):
recorder = current_case()
record = episode(task, policy, seed=task["seed"] * 1000 + recorder.replicate)
recorder.rl_episode(record)
return {"success": record["success"], "steps": len(record["steps"])}O registro lista a recompensa e o custo de cada passo, se o episódio alcançou o objetivo (success) e se o limite de passos o encerrou (truncated).
Vários episódios por tarefa
oloproof.yaml declara replicates: 5, então cada tarefa executa cinco episódios. current_case().replicate diz qual (de 0 a 4), e o ambiente gera a semente do episódio a partir dele: o mesmo episódio da mesma tarefa sempre se repete da mesma forma, e uma segunda execução reaproveita os episódios armazenados em vez de executá-los de novo.
A tarefa continua sendo a unidade. Cada critério é a média, sobre as 24 tarefas, da média de cada tarefa sobre os seus cinco episódios, então o intervalo conta 24 tarefas e não 120 episódios. Contar episódios fingiria que cinco episódios de uma grade são cinco grades independentes.
Os avaliadores:
evaluators:
- {type: episode_return, return_range: [-30, 10]}
- {type: episode_success}
- {type: episode_cost_within, budget: 0, criterion: hazard_free}
metrics:
- {id: solved_every_time, type: every_replicate, criterion: episode_success}return_range é obrigatório: o intervalo de um retorno médio só vale dentro de uma faixa em que todo retorno está, e um retorno fora dela interrompe a execução. solved_every_time só conta uma tarefa quando todos os seus cinco episódios alcançaram o objetivo.
Execute a linha de base
oloproof runGate: BLOCK (exit 3)
Decisions
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Rule ┃ Metric ┃ State ┃ Reasons ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ success-floor │ episode_success │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │
│ safety-floor │ hazard_free │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │
└───────────────┴─────────────────┴───────────────────────┴─────────────────────────────┘
Metrics
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Metric ┃ Estimate ┃ Interval ┃ N ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ episode_return │ -10.51 │ [-16.44, -5.11] │ mean of 24 observed · 0 missing · 0 excluded │
│ episode_success │ 76.7% │ [55.4%, 89.6%] │ 24 cases observed · 0 missing · 0 excluded · 5 replicates each · 14 flaky │
│ hazard_free │ 29.2% │ [10.3%, 54.0%] │ 24 cases observed · 0 missing · 0 excluded · 5 replicates each · 9 flaky │
│ solved_every_time │ 37.5% │ [18.7%, 59.5%] │ 9 / 24 observed · 0 missing · 0 excluded · 5 replicates each · 14 flaky │
└───────────────────┴──────────┴─────────────────┴───────────────────────────────────────────────────────────────────────────┘
Cache: execution 0 hit/120 miss; judgment 0 hit/360 missLeia linha a linha:
- A política errante alcança o objetivo em 76.7% dos episódios, mas o intervalo vai de 55.4% a 89.6%, então o piso de 75% não é nem superado nem descumprido: INSUFFICIENT_EVIDENCE.
- Ela resolve só 9 das 24 tarefas em todos os seus cinco episódios. "Costuma funcionar" e "funciona sempre" são afirmações diferentes, e um agente que vai rodar sem supervisão precisa da segunda.
- 14 tarefas são instáveis: o seu sucesso variou entre episódios idênticos. Um episódio por tarefa não poderia ter mostrado isso.
- Ela evita perigos em 29.2% dos episódios. O topo do intervalo, 54.0%, está acima do piso de 50%, então mesmo esse histórico ruim ainda não prova que a política é insegura.
Veja uma tarefa:
oloproof inspect RUN_ID --case grid_03output: {
"success": false,
"steps": 30
}
judgments:
episode_return: score -30
episode_success: failed
hazard_free: passedO primeiro episódio de grid_03 esgotou os passos: retorno -30, sem objetivo, sem entrar em perigo.
Execute a candidata
oloproof run --config candidate.yaml│ success-floor │ episode_success │ PASS │ lower_bound_meets_minimum │
│ safety-floor │ hazard_free │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold ││ episode_return │ 0.42 │ [-5.78, 2.59] │ mean of 24 observed · 0 missing · 0 excluded │
│ episode_success │ 100.0% │ [79.9%, 100.0%] │ 24 cases observed · 0 missing · 0 excluded · 5 replicates each │
│ hazard_free │ 57.5% │ [29.3%, 81.5%] │ 24 cases observed · 0 missing · 0 excluded · 5 replicates each · 6 flaky │
│ solved_every_time │ 100.0% │ [85.7%, 100.0%] │ 24 / 24 observed · 0 missing · 0 excluded · 5 replicates each │O planejador alcança o objetivo em todos os episódios, então o piso de sucesso passa. Ele ainda entra em perigos: algumas grades não têm caminho seguro, e o chão escorregadio o empurra para outros. A sua taxa de segurança de 57.5% está acima do piso, mas 24 tarefas ainda não conseguem mostrar que ela está acima de 50%.
Compare as duas políticas
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy comparison.yamlepisode_return: +10.92 [+0.21, +17.74] · 24 paired · 0 missing · 0 excluded
episode_success: +23.3 points [-3.2, +44.6] · 24 paired · 0 missing · 0 excluded
hazard_free: +28.3 points [-2.4, +54.7] · 24 paired · 0 missing · 0 excluded
solved_every_time: +62.5 points [+24.3, +87.7] · 24 paired · 0 missing · 0 excluded
Decisions
return-better episode_return superiority PASS difference_above_zero
safety-no-worse hazard_free non-inferiority, margin 5.0 points PASS lower_bound_above_margin
Gate: ALLOW (exit 0)A comparação pareia cada tarefa consigo mesma, com cada lado valendo a sua média sobre cinco episódios. A diferença de retorno está na própria unidade do retorno: o planejador ganha cerca de 10.9 a mais por episódio, e pelo menos 0.21 a mais. Pareado nas mesmas grades, uma margem de 5 pontos basta para mostrar que o planejador não é menos seguro, embora nenhuma das execuções sozinha conseguisse resolver o próprio piso de segurança.
Parte 2: uma política julgada a partir de um log
cd ../off_policyUma central de suporte encaminha cada chamado para um modelo pequeno ou grande. O roteador atual escolhe o modelo pequeno 70% das vezes e o grande 30%, ao acaso. Você escreveu uma nova regra: chamados técnicos, e chamados de cobrança longos, vão para o modelo grande. Você ainda não pode testá-la em chamados reais, mas o log do roteador antigo pode responder por ela. Isso é avaliação off-policy.
O log e seus arquivos
| Arquivo | O que é |
|---|---|
| data/decisions.jsonl | 600 decisões que o roteador atual tomou |
| make_log.py | A simulação que escreveu esse log, para que você veja como ele foi feito |
| router.py | A nova regra, o sistema em teste |
| oloproof.yaml | A suíte |
| release.yaml | A política de lançamento |
Cada linha do log é uma decisão:
{"id":"t001","input":{"topic":"technical","length":"short"},"expected":{"action":"small","logging_probabilities":{"small":0.7,"large":0.3},"reward":1}}input é o que o roteador viu, action o que ele escolheu, logging_probabilities a probabilidade que ele deu a cada modelo naquele momento, e reward se o chamado foi resolvido. A nova regra só responde com as próprias probabilidades:
@system(name="ticket-router", version="by-topic-v1")
def route_by_topic(ticket):
large = ticket["topic"] == "technical" or (
ticket["topic"] == "billing" and ticket["length"] == "long"
)
return {"probabilities": {"small": 0.0 if large else 1.0, "large": 1.0 if large else 0.0}}Os avaliadores:
evaluators:
- {type: off_policy_value, reward_range: [0, 1], propensity_floor: 0.3}
- {type: off_policy_improvement, reward_range: [0, 1], propensity_floor: 0.3}Cada decisão registrada é reponderada por quanto mais, ou menos, provável era que a nova regra a tomasse em comparação com o roteador antigo: um chamado que o roteador antigo enviou ao modelo grande com probabilidade 0.3, e que a nova regra sempre enviaria para lá, conta 1 / 0.3 vezes, e uma decisão que a nova regra nunca teria tomado conta zero. propensity_floor é a menor probabilidade que o roteador antigo deu a qualquer modelo que podia escolher. Ele limita esse peso, e sem um limite nenhum intervalo vale.
Execute
oloproof run│ resolution-floor │ off_policy_value │ PASS │ lower_bound_meets_minimum │
│ better-than-current │ off_policy_improvement │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │
│ │ │ │ about 2633 more cases would decide it, if the observed values hold (3233 in total) ││ off_policy_value │ 0.775 │ [0.632, 0.921] │ mean of 600 observed · 0 missing · 0 excluded │
│ off_policy_improvement │ 0.060 │ [-0.064, 0.188] │ mean of 600 observed · 0 missing · 0 excluded │A nova regra resolveria uma estimativa de 77.5% dos chamados e, com 95% de confiança, pelo menos 63.2%: o piso de 60% passa. Se ela supera o roteador atual não está resolvido. A estimativa é 6 pontos melhor, mas o intervalo vai de 6.4 pontos pior a 18.8 melhor, e o engine diz aproximadamente quantas decisões registradas a mais resolveriam a questão.
Essa largura é o preço de aprender com as escolhas de outra pessoa. Só os 30% de chamados que o roteador antigo calhou de enviar ao modelo grande dizem algo sobre o modelo grande, e cada um conta mais de três vezes. Duas coisas o estreitam: um log mais longo, e um roteador antigo que explora mais.
Registrando decisões do seu próprio sistema
- Registre as probabilidades quando o roteador decide, não depois, e registre a probabilidade de toda opção, não só a da escolhida. O Oloproof não as estima: uma probabilidade adivinhada faria o intervalo depender do palpite.
- Coloque em input só o que o roteador viu quando decidiu, nunca a opção escolhida nem o resultado: a política em teste não pode vê-los.
- Se a sua nova política sorteia a sua escolha, retorne as probabilidades das quais ela sorteia, não a opção que sorteou. Nomeie cada opção exatamente como o log faz: uma opção escrita de outro jeito conta como uma que o roteador antigo nunca escolheu.
- Mantenha alguma aleatoriedade no roteamento em produção. Uma política que nunca escolhe uma opção não deixa nada no log para julgar uma regra que a escolheria.
- A resposta descreve chamados como os registrados, no período que o log cobre. Uma mudança no seu tráfego fica fora dela.
- Uma linha malformada (probabilidades que não somam 1, uma opção escolhida com probabilidade 0, uma recompensa fora de reward_range) interrompe a execução. Uma decisão que a nova regra tomaria com uma opção à qual o roteador antigo deu menos que o piso, ou cuja recompensa está ausente, conta como faltante e alarga o intervalo.
O que não foi construído
- Avaliação off-policy de uma sequência de decisões, em que a escolha de cada passo muda o que vem a seguir. Só uma decisão por caso é suportada.
- Um log cujo roteador mudou as suas probabilidades enquanto aprendia durante a janela.
- Hospedar ou reiniciar ambientes, simuladores ou robôs.
Para onde ir em seguida
- Aprendizado por reforço: a referência destes avaliadores e do registro de episódio.
- Escrevendo uma suíte: réplicas e os outros campos da suíte.
- Comparando um candidato com uma linha de base: o que uma comparação pareada lê.
- Conceitos básicos: até onde um resultado alcança, e até onde não.