指南
教程:评估强化学习策略
一份针对两类强化学习问题的可运行演练:一个你能运行的策略在各回合上表现如何;以及一个你无法在真实世界中试用的策略本来会表现如何,依据它将要取代的那个策略的日志来评判。两个示例都在本地运行,无需提供方凭据。
每个字段和评估器的参考见强化学习;用例、评估器、指标、区间和门禁这些术语在核心概念中。本页是贯穿它们的动手路线。
Oloproof 在这里做什么、不做什么
Oloproof 不运行你的环境,不托管模拟器,也不训练策略。你的系统在它自己的环境中运行每个回合(episode),并把它记录为一个 rl_episode/v1 产物,每个回合指标都是从这份记录中读出来的。在第二部分中什么都不运行:被测策略只说明它会给每个动作的概率。
把两个问题分开:
| 问题 | 你拥有什么 | 部分 |
|---|---|---|
| 这个策略在各回合上表现如何? | 一个你能运行的策略和环境 | 1 |
| 这个策略在生产环境中本来会表现如何? | 一个较旧策略所做决策的日志,以及它使用的概率 | 2 |
前提条件
- Python 3.11 或更高版本,并已安装 Oloproof(pip install oloproof)。
- 示例项目,它随软件包一起提供。把它复制到一个新目录:
oloproof init --example rl my-rl它包含两个项目:episodes/ 和 off_policy/。下面的每条命令都在其中一个项目内运行,证据存储在该项目的 .oloproof/ 目录中。
第 1 部分:一个你能运行的策略
cd my-rl/episodes文件
| 文件 | 它是什么 |
|---|---|
| env.py | 一个网格世界、两个策略(wander 和 plan)以及运行它们的系统 |
| data/tasks.jsonl | 24 个网格,每个是一个任务:墙壁、危险格、一个起点和一个目标 |
| oloproof.yaml | 基线策略 wander 的测试套件 |
| candidate.yaml | 候选策略 plan 的同一个测试套件 |
| release.yaml | 发布策略 |
| comparison.yaml | 候选相对于基线必须满足的规则 |
一个回合每步移动一格,直到到达目标或用完步数。每一步得 -1,到达目标得 +10,因此回报(return)介于 -30(30 步,未到达目标)和 10 之间。进入危险格的成本为 1:这就是安全约束。十次移动中有一次会滑向随机方向,无论策略选择了什么,因此同一个策略在同一个网格上可能表现不同。
记录一个回合
系统自己运行回合,并把记录交给用例记录器:
@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"])}记录列出了每一步的奖励和成本、回合是否到达了目标(success),以及是否是步数上限结束了它(truncated)。
每个任务多个回合
oloproof.yaml 声明了 replicates: 5,因此每个任务运行五个回合。current_case().replicate 说明是哪一个(0 到 4),环境用它为回合设定种子:同一个任务的同一个回合总是以同样的方式重放,第二次运行会复用已存储的回合,而不是再次运行它们。
任务仍然是单位。每个判据都是 24 个任务上的平均值,而每个任务的值是它自己在五个回合上的平均值,因此区间计数的是 24 个任务,而不是 120 个回合。按回合计数等于假装一个网格的五个回合是五个独立的网格。
评估器:
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 是必需的:平均回报上的区间只在每个回报都落入的范围内成立,超出该范围的回报会停止运行。solved_every_time 只有在一个任务的全部五个回合都到达目标时才计入该任务。
运行基线
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 miss逐行阅读:
- 漫游策略在 76.7% 的回合中到达目标,但区间从 55.4% 到 89.6%,因此 75% 的下限既没有被达到,也没有被错过:INSUFFICIENT_EVIDENCE。
- 它只在 24 个任务中的 9 个上做到了五个回合每次都解决。"通常有效"和"每次都有效"是不同的主张,而一个将在无人看管下运行的智能体需要的是后者。
- 14 个任务不稳定:它们的成功与否在完全相同的回合之间发生了变化。每个任务只跑一个回合是无法显示这一点的。
- 它在 29.2% 的回合中避开了危险格。区间的上端 54.0% 高于 50% 的下限,因此即使是这样糟糕的记录,也还不能证明这个策略不安全。
查看一个任务:
oloproof inspect RUN_ID --case grid_03output: {
"success": false,
"steps": 30
}
judgments:
episode_return: score -30
episode_success: failed
hazard_free: passedgrid_03 的第一个回合用完了步数:回报 -30,没有到达目标,没有进入危险格。
运行候选
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 │规划策略在每个回合中都到达目标,因此成功率下限通过。它仍然会进入危险格:有些网格没有安全路径,湿滑的地面又会把它推进其他危险格。它 57.5% 的安全率高于下限,但 24 个任务还不能证明它高于 50%。
比较两个策略
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)比较把每个任务与它自己配对,每一侧取它在五个回合上的平均值。回报的差异以回报自身的单位表示:规划策略每个回合大约多得 10.9,至少多得 0.21。在相同的网格上配对后,5 个百分点的界值就足以表明规划策略的安全性不比基线差,尽管两次运行单独都无法确定各自的安全下限。
第 2 部分:依据日志评判的策略
cd ../off_policy一个客服台把每张工单路由到一个小模型或一个大模型。当前的路由器随机地在 70% 的时间里选择小模型,30% 的时间里选择大模型。你写了一条新规则:技术类工单以及较长的账单类工单交给大模型。你还不能在真实工单上试用它,但旧路由器的日志可以替它作答。这就是离策略评估(off-policy evaluation)。
日志及其文件
| 文件 | 它是什么 |
|---|---|
| data/decisions.jsonl | 当前路由器做出的 600 条决策 |
| make_log.py | 写出该日志的模拟程序,让你能看到它是如何生成的 |
| router.py | 新规则,即被测系统 |
| oloproof.yaml | 测试套件 |
| release.yaml | 发布策略 |
日志的每一行是一条决策:
{"id":"t001","input":{"topic":"technical","length":"short"},"expected":{"action":"small","logging_probabilities":{"small":0.7,"large":0.3},"reward":1}}input 是路由器看到的内容,action 是它的选择,logging_probabilities 是它在那一刻给每个模型的概率,reward 表示工单是否得到解决。新规则只用它自己的概率作答:
@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}}评估器:
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}每条已记录的决策都按新规则做出它的可能性比旧路由器高多少或低多少来重新加权:一张旧路由器以 0.3 的概率发给大模型、而新规则总会发往那里的工单,计为 1 / 0.3 次;一条新规则永远不会做出的决策计为零。propensity_floor 是旧路由器给它可能选择的任何模型的最小概率。它约束了该权重,没有约束,任何区间都不成立。
运行
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 │新规则估计能解决 77.5% 的工单,在 95% 置信水平下至少为 63.2%:60% 的下限通过。它是否胜过当前路由器尚无定论。估计值好 6 个百分点,但区间从差 6.4 个百分点到好 18.8 个百分点,引擎会说明大约还需要多少条已记录的决策才能作出决定。
这个宽度就是从别人的选择中学习的代价。只有旧路由器碰巧发给大模型的那 30% 的工单能说明大模型的情况,而且每一张都被计入三次以上。有两件事能让它变窄:更长的日志,以及探索更多的旧路由器。
从你自己的系统记录决策
- 在路由器做决策时记录概率,而不是事后记录;并记录每个选项的概率,而不只是被选中的那个。Oloproof 不估计它们:猜测的概率会让区间建立在这个猜测之上。
- 在 input 中只放入路由器做决策时看到的内容,绝不放入被选中的选项或结果:被测策略不得看到它们。
- 如果你的新策略通过采样做出选择,请返回它采样所依据的概率,而不是它抽到的那一个选项。每个选项的名称要与日志中的完全一致:拼写不同的选项被视为旧路由器从未选择过的选项。
- 在生产环境的路由中保留一定的随机性。一个从不选择某个选项的策略,不会在日志中留下任何东西来评判一个会选择它的规则。
- 答案描述的是与已记录工单相似的工单,范围是日志所覆盖的时期。你的流量发生的变化不在其中。
- 格式错误的行(概率总和不为 1、被选中选项的概率为 0、奖励超出 reward_range)会停止运行。新规则会选择一个旧路由器所给概率低于下限的选项的决策,或者奖励缺失的决策,都计为缺失并会使区间变宽。
尚未构建的功能
- 对一系列决策的离策略评估,其中每一步的选择都会改变接下来发生的事情。只支持每个用例一条决策。
- 路由器在时间窗口内一边学习一边改变概率的日志。
- 托管或重置环境、模拟器或机器人。
下一步
- 强化学习:这些评估器和回合记录的参考。
- 编写测试套件:重复测量以及其他测试套件字段。
- 将候选与基线进行比较:配对比较读取的是什么。
- 核心概念:结果能延伸到哪里、不能延伸到哪里。