本文へスキップ

ガイド

チュートリアル:分類器または構造化出力

サポートの質問にラベルを付ける Python 関数を評価し、リリースがブロックされる理由を読み、取りこぼしを直し、修正版を元の版と比較します。すべてあなたのマシン上で、アカウントもネットワークもモデルも使わずに行います。

作るもの

answer と label(refund、account、other のいずれか)を持つ JSON オブジェクトを返すサポートボットです。これに 3 つの要件を課します。ラベルが十分な頻度で正しいこと、出力が常に正しい形をしていること、そしてどの回答も米国の社会保障番号のように見えるものを漏らさないことです。このうち 2 つは参照回答を必要としない形式チェックで、1 つは参照ラベルに対してタスクの成功を測ります。この違いは重要であり、このページでは両者を分けて扱います。

以下で使う用語(ケース、実行、メトリクス、区間、ルール、ゲート)は 基本概念 で定義しています。

前提条件

  • Python 3.11 以降。
  • 仮想環境にインストールした Oloproof:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • パッケージに同梱されているサンプルプロジェクトと候補の変更。両方を新しいディレクトリにコピーし、1 つ目で作業します。すべてのファイルは下にも載せているので、手で入力することもできます:
oloproof init --example support_bot support-classifier
oloproof init --example classification support-change
cd support-classifier

このページでは、API キー、プロバイダーのアカウント、ネットワークアクセスは一切使いません。

ファイル

support-classifier/
  app.py              the application under test (a Python callable)
  oloproof.yaml       the suite: dataset, system, evaluators
  release.yaml        the release policy: rules the run is decided against
  data/support.jsonl  18 cases, one JSON object per line
  rubrics/helpful.md  a judge rubric, unused here

すべてのコマンドは support-classifier/ ディレクトリから実行してください。Oloproof はそこの .oloproof/ にストアを保持します。最初からやり直すには、そのディレクトリを削除してください。

アプリケーションとそのアダプター

アプリケーションにはアダプターを通じて到達します。Python アプリケーションの場合、アダプターは関数そのものです。Oloproof はそれをインポートし、ケースごとにそのケースの input を渡して 1 回呼び出し、返された辞書をそのケースの出力として記録します。

# app.py
from typing import Any

from oloproof import system


@system(name="support-bot", version="slice-a-example")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if "refund" in question:
        return {"answer": "Refunds are available within 30 days when the order is eligible.",
                "label": "refund"}
    if "password" in question or "login" in question:
        return {"answer": "Use password reset, then contact support if the login still fails.",
                "label": "account"}
    return {"answer": "A support specialist will follow up with the next step.", "label": "other"}

自分の分類器を評価するには、そのコードはそのままにして、それを呼び出して辞書を返す、このような薄い関数を書いてください。関数は async でもかまいません。Oloproof はそれを呼び出しますが、アプリケーションをホストも、サンドボックス化も、リセットもしません。そのため、アプリケーションが呼び出し間で保持する状態はあなたが管理します。

oloproof.yaml はその関数と評価器を指定します:

version: 1
project: support-bot-example
dataset: data/support.jsonl
system:
  name: support-bot
  version: slice-a-example
  callable: app:answer
  timeout_s: 30
evaluators:
  - type: exact_match
    criterion: exact_label
    field: label
  - type: json_schema
    criterion: format_valid
    field: null
    schema:
      type: object
      required: [answer, label]
      properties:
        answer: {type: string}
        label: {type: string}
      additionalProperties: false
  - type: regex
    criterion: pii_free
    field: answer
    pattern: '\b\d{3}-\d{2}-\d{4}\b'
    pass_if: no_match

出力は、関数のソース、宣言された version、config をキーとしてキャッシュされます。関数が他のファイル(プロンプト、ルール表)を読む場合は、それらを system.code_paths に列挙してください。そうすれば、それらを編集したときにシステムが再実行されます。

データセット

1 行に 1 ケースです。input は関数が case として受け取るものそのもので、expected は exact_match 評価器が比較する参照です:

{"id":"refund_00","input":{"question":"Can I get a refund for yesterday's order?"},"expected":{"label":"refund"}}
{"id":"account_04","input":{"question":"I can't sign in on my new phone."},"expected":{"label":"account"}}
{"id":"other_04","input":{"question":"I don't want a refund, I just need a copy of my receipt."},"expected":{"label":"other"}}

関数は各ケースに対して、{"answer": "Use password reset, ...", "label": "account"} のようなオブジェクトを返します。

評価器を選ぶ

基準評価器expected が必要か測るもの
exact_labellabel に対する exact_matchはいタスクの成功:ラベルが正しいこと
format_valid出力全体に対する json_schemaいいえ形式:オブジェクトがちょうど 2 つの文字列フィールドを持つこと
pii_freeanswer に対する regex、pass_if: no_matchいいえテキストの安全性に関する性質

形式チェックは、形は正しいが誤った回答も合格にするため、タスクの成功の代わりには決してなりません。タスクチェックはすべてのケースに参照を必要とし、参照のないケースは exact_match で採点できません。決定的な評価器は人による検証を必要としません。2 回実行しても同じ判定になります。

ポリシー

release.yaml は実行の判断基準です:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
warn_on: []
rules:
  - id: exact-label-floor
    metric: exact_label
    min: 0.70
  - id: valid-format
    metric: format_valid
    kind: observed_count
    max_failures: 0
  - id: pii-free
    metric: pii_free
    kind: observed_count
    max_failures: 0

exact-label-floor は、ラベルが少なくとも 70% の割合で正しくなければならないと定め、95% 区間全体が 0.70 以上のときにのみ合格します。2 つの observed_count ルールは、実行したケースで失敗を一切許しません。これらはこのケースについて述べるもので、ユーザーが尋ねるすべての質問について述べるものではありません。

実行する

oloproof run

実際の出力(一部省略):

Run run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ exact-label-floor │ exact_label  │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ valid-format      │ format_valid │ PASS                  │ observed_failures_within_limit │
│ pii-free          │ pii_free     │ PASS                  │ observed_failures_within_limit │
│ exact_label  │ 72.2%    │ [46.5%, 90.4%]  │ 13 / 18 observed · 0 missing · 0 excluded │
│ format_valid │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
│ pii_free     │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/18 miss; judgment 0 hit/54 miss

読み方:

  • 18 個中 13 個のラベルが正しく、72.2% です。これは 0.70 を上回りますが、区間は 46.5% まで下がります。18 ケースでは、真の割合が少なくとも 0.70 であることを示せません。そのためルールは INSUFFICIENT_EVIDENCE であり、PASS でも FAIL でもありません。
  • すべての出力が正しい形をしており、SSN のような番号を含むものもないため、両方の形式ルールは合格します。
  • block_on に INSUFFICIENT_EVIDENCE が含まれているので、ゲートはブロックし、コマンドは 3 で終了します。終了コード 0 は、ポリシーがブロックするものが何もないことを意味します。すべてのコードは CI のゲート に載っています。

もう一度実行すると、キャッシュの行は execution 18 hit/0 miss になります。何も変わっていないので、関数は呼び出されません。

失敗を調べる

oloproof inspect RUN_ID --failures
oloproof inspect RUN_ID --case refund_04

RUN_ID は、実行の出力の最初の行にある ID です。

5 of 18 cases failed, errored or did not finish

refund_04
  output: {"answer": "A support specialist will follow up with the next step.", "label": "other"}
  exact_label: failed
...
other_04
  output: {"answer": "Refunds are available within 30 days when the order is eligible.", "label": "refund"}
  exact_label: failed
case refund_04
input: {
  "question": "I was charged twice this month and want my money back."
}
expected: {
  "label": "refund"
}
execution: OK, 1 ms
output: {
  "answer": "A support specialist will follow up with the next step.",
  "label": "other"
}
judgments:
  exact_label: failed
  format_valid: passed
  pii_free: passed

入力を読めばパターンは明らかです。「money back」「reverse the payment」「sign in」「two-factor」はキーワードリストになく、other_04 は「I don't want a refund」と言っているのに、「refund」という語にマッチしてしまいます。refund_04 は誤っているのに両方の形式チェックに合格している点に注意してください。これが、形式の確認と成功の測定の差です。

ここで意味のある次の行動は 2 つあります。取りこぼしを直す(下記)か、ケースを追加するかです。同じ正解率でケースが増えれば区間は狭まり、oloproof plan RUN_ID --run が何件必要かを見積もります。

実際に変更する

../support-change/app.py を app.py に上書きコピーします。取りこぼした言い回しが追加されています:

REFUND_WORDS = ("refund", "money back", "reverse the payment")
ACCOUNT_WORDS = ("password", "login", "sign in", "two-factor")


@system(name="support-bot", version="keywords-v2")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if any(word in question for word in REFUND_WORDS):
        ...

そして oloproof.yaml の system の下で version: keywords-v2 を設定し、実行が新しいバージョンとして記録されるようにします。続いて:

oloproof run
Gate: ALLOW (exit 0)
│ exact-label-floor │ exact_label  │ PASS  │ lower_bound_meets_minimum      │
│ exact_label  │ 94.4%    │ [72.7%, 99.9%]  │ 17 / 18 observed · 0 missing · 0 excluded │

18 個中 17 個が正しく、区間の下限 72.7% が 0.70 を上回るので、ルールは合格し、コマンドは 0 で終了します。other_04 はまだ失敗します。修正は否定表現に手を付けていません。

候補をベースラインと比較する

実行ルールが問うのは、候補が下限を満たすかどうかです。比較が問うのは、候補がベースラインとケースごとにどう違うかです。../support-change/compare.yaml をプロジェクトにコピーしてください。比較ルールが 1 つ入っています:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: label-no-regression
    kind: non_inferiority
    metric: exact_label
    margin: 0.10
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
Comparison sha256:de76... of run_01M4...TJAD against run_01M4...ECVEF · 18 paired cases
exact_label: +22.2 points [-12.9, +57.0] · 18 paired · 0 missing · 0 excluded
format_valid: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
pii_free: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
Decisions
  label-no-regression  exact_label  non-inferiority, margin 10.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    about 3 more paired cases would decide it, if the difference holds (21 in total at 22% discordance)
Gate: BLOCK (exit 3)

候補は 4 ケースを直し、1 つも壊さず、推定で 22 ポイントの改善です。しかし変わったのは 4 ケースだけで、18 個の対応のあるケースでは、区間は 12.9 ポイント悪化から 57 ポイント改善までとなり、10 ポイントのマージンをまたぎます。比較は、候補が許容範囲を超えて悪いという可能性をまだ否定できないため、INSUFFICIENT_EVIDENCE となり 3 で終了します。その下の行はサイズの見積もりです。--policy を付けない場合、compare は差を表示し、プロジェクトの release.yaml が比較ルールを宣言していないことを伝え、何も判断されていないので 0 で終了します。

候補をベースラインと比較する で、マージンとその他のルールの種類を説明しています。

トラブルシューティング

症状原因と対処
app について ModuleNotFoundErrorapp.py があるディレクトリから実行するか、そこからインポート可能なモジュールパスを callable に指定してください。
ルールが、どの評価器も生成しないメトリクスを指定しているルールの metric は評価器の criterion と一致する必要があります。エラーには存在するメトリクスが列挙されます。
分類器を編集したのに、実行がすべての出力を再利用したキャッシュは callable のソースに従います。それが読むヘルパーファイルは system.code_paths に列挙する必要があります。
exact_label がケースを欠損として報告するそれらの実行は例外を送出したかタイムアウトしました。oloproof inspect RUN_ID --failures で各エラーを確認できます。
推定値が高いのに実行が 3 で終了する判断するのは推定値ではなく区間です。ケースを追加するか、実行前に決めたうえで低い下限を受け入れてください。

制限事項

  • SDK と YAML は評価器ごとの合格率を報告します。このような分類器に対する混同行列やクラスごとの適合率・再現率はありません。スコアを出すモデルについては predictive: ブロックがそれらを扱います(予測モデル)。
  • observed_count ルールは実行したケースについて述べるもので、未知の入力については何も主張しません。
  • 18 ケースの比較で判別できるのは大きな差だけです。50 件以上の実際のケースのほうが、より有用な下限です。
  • oloproof.yaml はカスタムの @evaluator を指定できません。それには SDK が必要です(SDK)。