[翻訳] DSPyにおける評価

[Evaluation Overview \- DSPy](https://dspy.ai/learn/evaluation/overview/)以降のセクション[2025/1/15時点]の翻訳です。

:::note warn
本書は著者が手動で翻訳したものであり内容の正確性を保証するものではありません。正確な内容に関しては原文を参照ください。
:::

:::note info
[\[翻訳\] DSPyを学ぶ](https://qiita.com/taka_yayoi/items/df31bd8f1918855fd552)のコンテンツです。
:::

# 評価の概要

初期システムを手に入れたら、次はよりシステマティックに洗練できるように**初期開発セットを収集する**時間です。あなたのタスクにおいて、200の例を集めるのは時間を要しますが、20の入力例でも有用となりえます。あなたの*メトリック*に基づき、ラベルが全くない入力のみを必要とするか、入力とあなたのシステムの*最終的な*出力を必要とします。(DSPyにおけるプログラムの中間ステップにおいては、ほとんどラベルを必要とすることはありません。)取り組んでいるタスクに近しいデータセットを見つけ出すことができるでしょうし、HuggingFaceデータセットやStackExchangeのように自然発生しているソースを活用することもできます。ライセンス的に十分利用できるデータがある場合には、それらを使うことをお勧めします。そうでない場合には、手動でいくつかのサンプルにラベルをつけ、システムのデモのデプロイをスタートし、その途中で初期データを収集しましょう。

次に、**あなたのDSPyメトリックを定義**する必要があります。あなたのシステムの出力の良し悪しは何によって決定されますか？メトリクスの定義と時間をかけたインクリメンタルな改善に投資しましょう。定義できないものを継続的に改善することは困難です。メトリックとは、あなたのデータからサンプルと、あなたのシステムのアウトプットを受け取り、スコアを返す関数です。シンプルな分類や短い形態のQAタスクのようなシンプルなタスクにおいては、これは単なる「精度」となることがあります。多くのアプリケーションにおいては、長い形態の出力を生成するので、使うことになるメトリックは出力の複数のプロパティをチェックする小さなDSPyプログラムとなるでしょう。最初のトライからこれを適切に行うことはまず無いでしょう: シンプルな何かからスタートして繰り返しましょう。

いくつかのデータとメトリックを手に入れたので、これらのトレードオフを理解するために、あなたのパイプラインのデザインに対する開発評価を実行します。出力とメトリックのスコアを確認しましょう。おそらく、これによって主要な問題を特定することができ、次のステップにおけるベースラインを定義することになります。

:::note info
**お使いのメトリック自身がDSPyプログラムの場合は...**
メトリック自身がDSPyプログラムの場合、イテレーションを行うパワフルな手段は、あなたのメトリック自身を最適化することです。メトリックの出力は通常シンプルな値なので、これは簡単なことであり、いくつかのサンプルを収集することで、簡単にメトリックのメトリックを定義し、最適化することができます。
:::

# データハンドリング

DSPyは機械学習フレームワークなので、取り扱いにはトレーニングセット、開発セット、テストセットが関係します。あなたのデータのそれぞれのサンプルに対して、通常3つのタイプの値を区別します: 入力、中間ラベル、そして最終的なラベルです。中間ラベルや最終的なラベルなしにDSPyを効果的に使うことはできますが、少なくともいくつかの入力サンプルは必要となります。

## DSPyの`Example`オブジェクト

DSPyにおけるデータのコアとなるデータタイプは`Example`です。あなたのトレーニングセットとテストセットのアイテムを表現するために**Examples**を使うことになります。

DSPyの**Examples**はPythonの`dict`と似ていますが、いくつかの有用なユーティリティを提供しています。あなたのDSPyモジュールは、`Example`の特殊なサブクラスである`Prediction`タイプの値を返却します。

DSPyを使う際、多くの評価ランや最適化ランを実行します。あなたの個々のデータポイントは`Example`タイプとなります:

```py
qa_pair = dspy.Example(question="This is a question?", answer="This is an answer.")

print(qa_pair)
print(qa_pair.question)
print(qa_pair.answer)
```

**出力:**

```
Example({'question': 'This is a question?', 'answer': 'This is an answer.'}) (input_keys=None)
This is a question?
This is an answer.
```

Exampleにはいくつかのフィールドキーと、多くは文字列型ですが値の型が含まれます。

```py
object = Example(field1=value1, field2=value2, field3=value3, ...)
```

以下のようにトレーニングセットのサンプルを表現することができます:

```py
trainset = [dspy.Example(report="LONG REPORT 1", summary="short summary 1"), ...]
```

## 入力キーの指定

従来のMLでは、分離された「入力」と「ラベル」があります。

DSPyでは、`Example`オブジェクトには入力として特定のフィールドをマークできる`with_inputs()`メソッドがあります。

```py
# Single Input.
print(qa_pair.with_inputs("question"))

# Multiple Inputs; be careful about marking your labels as inputs unless you mean it.
print(qa_pair.with_inputs("question", "answer"))
```

`.`(ドット)オペレーターで値にアクセスできます。`object.name`を通じて、定義されたオブジェクト`Example(name="John Doe", job="sleep")`のキーである`name`の値にアクセスできます。

特定のキーにアクセスしたり除外するには、入力のみを含む新たなExampleオブジェクトを返却する`inputs()`、入力を含まないキーのみを含むExampleオブジェクトを返却する`labels()`メソッドを使います。

```py
article_summary = dspy.Example(article= "This is an article.", summary= "This is a summary.").with_inputs("article")

input_key_only = article_summary.inputs()
non_input_key_only = article_summary.labels()

print("Example object with Input fields only:", input_key_only)
print("Example object with Non-Input fields only:", non_input_key_only)
```

**出力:**

```
Example object with Input fields only: Example({'article': 'This is an article.'}) (input_keys=None)
Example object with Non-Input fields only: Example({'summary': 'This is a summary.'}) (input_keys=None)
```

# メトリクス

DSPyは機械学習フレームワークなので、(あなたの進捗を追跡するための)評価と(DSPyがあなたのプログラムをより効果的なものにする)最適化のための**自動化メトリクス**を考える必要があります。

## メトリックとは何で、自分のタスクのメトリックをどのように定義するのか？

メトリックは、あなたのデータからのサンプルと、あなたのシステムの出力を受け取り、出力がどれだけ良かったかを定量化するスコアを返却する単なる関数です。あなたのシステムの出力が良いか悪いかはどのように決定されるのでしょうか？

シンプルなタスクにおいては、これは単なる「精度」、「正確一致」、「F1スコア」である場合があります。これは、シンプルな分類や短い形態のSAタスクで該当することがあります。

しかし、多くのアプリケーションにおいては、あなたのシステムは長い形態の出力を生成します。ここでは、あなたのメトリックは出力の複数のプロパティをチェック(LMからのAIフィードバックを用いる可能性が高いです)する小規模なDSPyプログラムとなることでしょう。

最初のトライでこれを適切に行うことはまずありませんが、シンプルなものからスタートして繰り返すべきです。

## シンプルなメトリクス

DSPyのメトリックは`example`(トレーニングセットや開発セットなど)とあなたのDSPyプログラムの出力`pred`を受け取り、`float`(あるいは`int`や`bool`)のスコアを出力する単なるPythonの関数です。

また、あなたのメトリックは`trace`と呼ばれる3番目のオプションの引数を受けつ必要があります。この時点ではこれを無視しても構いませんが、最適化であなたのメトリックを使いたいと考えた際には、いくつかのパワフルなトリックを使えるようになります。

`example.answer`と`pred.answer`を比較するシンプルなメトリックの例を示します。この特定のメトリックは`bool`を返却します。

```py
def validate_answer(example, pred, trace=None):
    return example.answer.lower() == pred.answer.lower()
```

何人かの方は、これらの(ビルトイン)ユーティリティが便利だと思うことでしょう:

- `dspy.evaluate.metrics.answer_exact_match`
- `dspy.evaluate.metrics.answer_passage_match`

複数のプロパティをチェックするなど、メトリックをさらに複雑にすることもできます。以下のメトリックは、`trace is None`(評価や最適化で使用される場合)の場合には`float`を返却し、それ以外の場合(デモンストレーションのブートストラップで使用)には`bool`を返却します。

```py
def validate_context_and_answer(example, pred, trace=None):
    # check the gold label and the predicted answer are the same
    answer_match = example.answer.lower() == pred.answer.lower()

    # check the predicted answer comes from one of the retrieved contexts
    context_match = any((pred.answer.lower() in c) for c in pred.context)

    if trace is None: # if we're doing evaluation or optimization
        return (answer_match + context_match) / 2.0
    else: # if we're doing bootstrapping, i.e. self-generating good demonstrations of each step
        return answer_match and context_match
```

優れたメトリックを定義することは繰り返しのプロセスなので、いくつかの初期評価を行い、あなたのデータと出力に着目することが鍵となります。

## 評価

メトリックを手に入れたら、シンプルなPythonループで評価を実行することができます:

```py
scores = []
for x in devset:
    pred = program(**x.inputs())
    score = metric(x, pred)
    scores.append(score)
```

いくつかのユーティリティが必要な場合には、ビルトインの`Evaluate`ユーティリティを活用することもできます。並列の評価(マルチスレッド)や入力/出力のサンプルやメトリックのスコアの表示のような事柄の助けとなります。

```py
from dspy.evaluate import Evaluate

# Set up the evaluator, which can be re-used in your code.
evaluator = Evaluate(devset=YOUR_DEVSET, num_threads=1, display_progress=True, display_table=5)

# Launch evaluation.
evaluator(YOUR_PROGRAM, metric=YOUR_METRIC)
```

## 中級: メトリックに対するAIのフィードバックの活用

多くのアプリケーションにおいては、あなたのシステムは長い形態の出力を生成するので、あなたのメトリックはLMからのAIフィードバックを用いて出力の複数の次元をチェックする必要があります。

このシンプルなシグネチャが役に立つことでしょう。

```py
# Define the signature for automatic assessments.
class Assess(dspy.Signature):
    """Assess the quality of a tweet along the specified dimension."""

    assessed_text = dspy.InputField()
    assessment_question = dspy.InputField()
    assessment_answer: bool = dspy.OutputField()
```

例えば、以下のメトリックは生成されたツイートが(1)指定された質問に適切に回答しているのか、(2)興味深いものであるかどうかをチェックしています。また、(3)文字数が`len(tweet) <= 280`であることもチェックしています。

```py
def metric(gold, pred, trace=None):
    question, answer, tweet = gold.question, gold.answer, pred.output

    engaging = "Does the assessed text make for a self-contained, engaging tweet?"
    correct = f"The text should answer `{question}` with `{answer}`. Does the assessed text contain this answer?"

    correct =  dspy.Predict(Assess)(assessed_text=tweet, assessment_question=correct)
    engaging = dspy.Predict(Assess)(assessed_text=tweet, assessment_question=engaging)

    correct, engaging = [m.assessment_answer for m in [correct, engaging]]
    score = (correct + engaging) if correct and (len(tweet) <= 280) else 0

    if trace is not None: return score >= 2
    return score / 2.0
```

コンパイルの際は`trace is not None`であり、事柄を判定することに関して厳密にしたいと考えるので、`score >= 2`の場合に`True`だけを返却します。それ以外の場合には、最大1.0のスコア(`score / 2.0`)を返却します。

## 上級: メトリックとしてDSPyプログラムを使用

あなたのメトリック自体ががDSPyプログラムの場合、イテレーションを行う最もパワフルな手段は、あなたのメトリック自身をコンパイル(最適化)することです。メトリックの出力は通常はシンプルな値(最大5のスコアなど)なので、メトリックのメトリックを定義し、いくつかのサンプルを収集して最適化することは簡単です。

### 上級: `trace`へのアクセス

評価ランの過程であなたのメトリックが使用される際、DSPyはあなたのプログラムのステップを追跡しようとはしません。

しかし、コンパイル(最適化)の際、DSPyはあなたのLMコールをトレースします。このトレースには、それぞれのDSPyの予測器の入力/出力を含んでいるので、最適化のために中間ステップを検証するためにそれらを活用することができます。

```py
def validate_hops(example, pred, trace=None):
    hops = [example.question] + [outputs.query for *_, outputs in trace if 'query' in outputs]

    if max([len(h) for h in hops]) > 100: return False
    if any(dspy.evaluate.answer_exact_match_str(hops[idx], hops[:idx], frac=0.8) for idx in range(2, len(hops))): return False

    return True
```