Pular para o conteúdo

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:

PerguntaO que você temParte
Quão bem esta política se sai ao longo de episódios?Uma política e um ambiente que você pode executar1
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 usou2

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-rl

Ele 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/episodes

Os arquivos

ArquivoO que é
env.pyUm mundo em grade, duas políticas (wander e plan) e os sistemas que as executam
data/tasks.jsonl24 grades, cada uma uma tarefa: paredes, perigos, um início e um objetivo
oloproof.yamlA suíte para a política de linha de base, wander
candidate.yamlA mesma suíte para a política candidata, plan
release.yamlA política de lançamento
comparison.yamlAs 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 run
Gate: 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 miss

Leia 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_03
output: {
  "success": false,
  "steps": 30
}
judgments:
  episode_return: score -30
  episode_success: failed
  hazard_free: passed

O 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.yaml
episode_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_policy

Uma 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

ArquivoO que é
data/decisions.jsonl600 decisões que o roteador atual tomou
make_log.pyA simulação que escreveu esse log, para que você veja como ele foi feito
router.pyA nova regra, o sistema em teste
oloproof.yamlA suíte
release.yamlA 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