本文へスキップ

ガイド

チュートリアル: 強化学習の方策を評価する

強化学習の 2 種類の問いについて、実際に動かして進む手順です。1 つは、実行できる方策がエピソードを通じてどれだけうまく機能するか。もう 1 つは、現実の世界で試せない方策が、それが置き換える方策のログから判断して、どれだけうまくいったはずか、です。どちらの例も、プロバイダーの認証情報なしにローカルで実行できます。

すべてのフィールドと評価器のリファレンスは強化学習です。ケース、評価器、メトリクス、区間、ゲートといった用語は基本概念にあります。このページは、それらを実際に手を動かしてたどる道筋です。

ここで Oloproof が行うこと、行わないこと

Oloproof はあなたの環境を実行せず、シミュレーターをホストせず、方策を学習させません。あなたのシステムが各エピソードを自身の環境で実行し、それを rl_episode/v1 アーティファクトとして記録します。エピソードのメトリクスはすべてそのレコードから読み取られます。パート 2 では何も実行されません。テスト対象の方策は、各行動に与える確率を述べるだけです。

2 つの問いを分けて考えてください。

問い手元にあるものパート
この方策はエピソードを通じてどれだけうまく機能するか実行できる方策と環境1
この方策は本番環境でどれだけうまくいったはずか古い方策が行った決定のログと、それが使った確率2

前提条件

  • Python 3.11 以降と、インストール済みの Oloproof(pip install oloproof)。
  • パッケージに同梱されているサンプルプロジェクト。新しいディレクトリにコピーしてください。
oloproof init --example rl my-rl

そこには episodes/ と off_policy/ の 2 つのプロジェクトがあります。以下のコマンドはすべてそのどちらかの中から実行し、エビデンスはその .oloproof/ ディレクトリに保存されます。

パート 1: 実行できる方策

cd my-rl/episodes

ファイル

ファイル内容
env.pyグリッドワールド、2 つの方策(wander と plan)、そしてそれらを実行するシステム
data/tasks.jsonl24 個のグリッド。それぞれが 1 つのタスクで、壁、危険地帯、スタート、ゴールを持つ
oloproof.yamlベースラインの方策 wander のスイート
candidate.yaml候補の方策 plan のための同じスイート
release.yamlリリースポリシー
comparison.yaml候補がベースラインに対して満たすべきルール

エピソードは、ゴールに到達するかステップを使い果たすまで、1 ステップに 1 マスずつ移動します。各ステップで -1、ゴールに到達すると +10 が得られるので、リターンは -30(30 ステップでゴールなし)から 10 の間にあります。危険地帯に入るとコストが 1 かかります。これが安全上の制約です。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 を宣言しているので、すべてのタスクで 5 つのエピソードを実行します。current_case().replicate はどのエピソードか(0 から 4)を示し、環境はこれをもとにエピソードのシードを決めます。同じタスクの同じエピソードは常に同じように再現され、2 回目の実行は保存済みのエピソードを再び実行せずに再利用します。

単位はタスクのままです。各基準は、各タスクの 5 つのエピソードにわたるそのタスク自身の平均を、24 のタスクにわたって平均したものなので、区間は 120 のエピソードではなく 24 のタスクを数えます。エピソードを数えると、1 つのグリッドの 5 つのエピソードを、5 つの独立したグリッドであるかのように装うことになります。

評価器は次のとおりです。

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 は、5 つのエピソードすべてがゴールに到達したときにだけタスクを数えます。

ベースラインを実行する

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

1 行ずつ読んでいきます。

  • さまよう方策はエピソードの 76.7% でゴールに到達しますが、区間は 55.4% から 89.6% に及ぶため、75% の下限はクリアされたとも未達だとも言えません。INSUFFICIENT_EVIDENCE です。
  • 5 つのエピソードのすべてで解けるのは、24 のタスクのうち 9 つだけです。「たいていうまくいく」と「毎回うまくいく」は別の主張であり、無人で動くエージェントに必要なのは後者です。
  • 14 のタスクが不安定です。同一のエピソードの間で成功が変わりました。タスクごとに 1 エピソードでは、それは見えなかったはずです。
  • 危険地帯を避けられたのはエピソードの 29.2% です。区間の上端の 54.0% は 50% の下限を上回るため、この悪い記録でさえ、方策が安全でないことはまだ証明していません。

1 つのタスクを見てみます。

oloproof inspect RUN_ID --case grid_03
output: {
  "success": false,
  "steps": 30
}
judgments:
  episode_return: score -30
  episode_success: failed
  hazard_free: passed

grid_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% を上回ることをまだ示せません。

2 つの方策を比較する

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)

比較は各タスクをそれ自身と対にし、各側の値は 5 つのエピソードにわたる平均です。リターンの差はリターン自身の単位で表されます。計画する方策はエピソードあたり約 10.9 多く、少なくとも 0.21 多く獲得します。同じグリッドで対にすると、どちらの実行も単独では自身の安全の下限に決着をつけられなかったにもかかわらず、5 ポイントのマージンで、計画する方策の安全性が劣らないことを示すのに十分です。

パート 2: ログから判断する方策

cd ../off_policy

サポートデスクは、各チケットを小さいモデルか大きいモデルに振り分けます。現在のルーターは、ランダムに、70% の確率で小さいモデルを、30% の確率で大きいモデルを選びます。あなたは新しいルールを書きました。技術的なチケットと、長い請求関連のチケットは大きいモデルに送る、というものです。まだ実際のチケットで試すことはできませんが、古いルーターのログがその代わりに答えてくれます。これがオフポリシー評価です。

ログとそのファイル

ファイル内容
data/decisions.jsonl現在のルーターが行った 600 件の決定
make_log.pyそのログを書いたシミュレーション。どう作られたかを確認できます
router.py新しいルール。テスト対象のシステム
oloproof.yamlスイート
release.yamlリリースポリシー

ログの各行は 1 つの決定です。

{"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 回分として数えられ、新しいルールなら決して下さなかった決定は 0 回分として数えられます。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% のチケットだけであり、それぞれが 3 倍以上に数えられます。区間を狭めるものは 2 つあります。より長いログと、より多く探索する古いルーターです。

自分のシステムから決定を記録する

  • 確率は後からではなく、ルーターが決定するときに記録してください。また、選ばれた選択肢の確率だけでなく、すべての選択肢の確率を記録してください。Oloproof はそれらを推定しません。推測した確率では、区間がその推測の上に成り立つことになります。
  • input には、ルーターが決定したときに見たものだけを入れ、選ばれた選択肢や結果は決して入れないでください。テスト対象の方策はそれらを見てはなりません。
  • 新しい方策が選択をサンプリングする場合は、引いた 1 つの選択肢ではなく、サンプリングのもとになる確率を返してください。各選択肢はログでの名前とまったく同じに名付けてください。綴りの異なる選択肢は、古いルーターが一度も選ばなかった選択肢として数えられます。
  • 本番のルーティングにはある程度のランダム性を残してください。ある選択肢を決して選ばない方策は、その選択肢を選ぶルールを判断する材料を、ログに何も残しません。
  • 答えが述べるのは、ログがカバーする期間における、記録されたチケットに似たチケットについてです。トラフィックの変化はその範囲の外にあります。
  • 形式の正しくない行(合計が 1 にならない確率、確率 0 の選ばれた選択肢、reward_range の外の報酬)は実行を停止させます。新しいルールが、古いルーターが下限未満の確率しか与えなかった選択肢で下すことになる決定、または報酬がない決定は、欠測として数えられ、区間を広げます。

未実装のもの

  • 各ステップの選択が次に起こることを変える、一連の決定のオフポリシー評価。ケースごとに 1 つの決定のみがサポートされます。
  • 期間中に学習しながら確率を変えたルーターのログ。
  • 環境、シミュレーター、ロボットのホストやリセット。

次に読むもの