DatabricksでDSPyのマルチホップQ&Aのサンプルを動かす

こちらの続きです。

https://qiita.com/taka_yayoi/items/2985f7ad17062446efab

https://qiita.com/taka_yayoi/items/a9827246cd214f3f8f4e

こちらをウォークスルーします。

https://dspy-docs.vercel.app/docs/tutorials/simplified-baleen

# [02] Multi-Hop Question Answering

複雑なQAタスクにおいては単一の検索クエリーでは不十分なことがあります。例えば、`HotPotQA`のサンプルには、"Right Back At It Again"の著者の出生地に関する質問が含まれています。多くの場合、検索クエリーは著者の"Jeremy McKinnon"を特定できますが、彼がどこで生まれたのかに関する意図された回答を構成する能力には欠けています。

retrieval-augmented NLPの文献においてこの課題に対する標準的なアプローチは、GoldEn(Qi et al., 2019)やBaleen(Khattab et al., 2021)のようなマルチホップの検索システムを構築するというものです。これらのシステムは最終的な回答に到達する前に、必要に応じて追加情報を取得するために、検索結果を読み込んで追加のクエリーを生成します。DSPyを用いることで、数行のコードで容易にこのようなシステムをシミュレートすることができます。

```py
%pip install dspy-ai
dbutils.library.restartPython()
```

# LMとRMの設定

様々な[LM](https://dspy-docs.vercel.app/docs/category/language-model-clients)、[RM](https://dspy-docs.vercel.app/docs/category/retrieval-model-clients)のAPI、[ローカルモデルのホスティング](https://dspy-docs.vercel.app/docs/category/local-language-model-clients)によって**DSPy**がサポートする言語モデル(LM)と検索モデル(RM)のセットアップからスタートします。

このノートブックでは、GPT-3.5(`gpt-3.5-turbo`)と`ColBERTv2`リトリーバー(こちらの[2017 dump](https://hotpotqa.github.io/wiki-readme.html)のそれぞれの記事の最初の段落を含むWikipedia 2017の"abstracts"検索インデックスをホストする無料のサーバー)を取り扱います。DSPyでLMとRMを設定することで、DSPyは生成や検索で必要となった際にそれぞれのモジュールを内部的に呼び出せるようになります。

```py
import os
os.environ["OPENAI_API_KEY"] = dbutils.secrets.get("demo-token-takaaki.yayoi", "openai_api_key")
```

```py
import dspy

turbo = dspy.OpenAI(model='gpt-3.5-turbo')
colbertv2_wiki17_abstracts = dspy.ColBERTv2(url='http://20.102.90.50:2017/wiki17_abstracts')

dspy.settings.configure(lm=turbo, rm=colbertv2_wiki17_abstracts)
```

# データセットのロード

このチュートリアルでは、マルチホップの形式で通常回答される複雑な質問-解答のペアのコレクションである上述の`HotPotQA`データセットを活用します。`HotPotQA`クラスを通じてDSPyが提供するこのデータセットをロードすることができます:

```py
from dspy.datasets import HotPotQA

# Load the dataset.
dataset = HotPotQA(train_seed=1, train_size=20, eval_seed=2023, dev_size=50, test_size=0)

# Tell DSPy that the 'question' field is the input. Any other fields are labels and/or metadata.
trainset = [x.with_inputs('question') for x in dataset.train]
devset = [x.with_inputs('question') for x in dataset.dev]

len(trainset), len(devset)
```

# シグネチャの構築

データをロードしたので、Baleenパイプラインのサブタスクのためのシグネチャの定義からスタートしましょう。

入力として`context`と`question`を受け取り、出力として`answer`を提供する`GenerateAnswer`のシグネチャの作成からスタートします。

```py
class GenerateAnswer(dspy.Signature):
    """簡潔な事実に基づく回答で質疑応答"""

    context = dspy.InputField(desc="may contain relevant facts")
    question = dspy.InputField()
    answer = dspy.OutputField(desc="often between 1 and 5 words")
```

通常のQAパイプラインと異なり、"hop"の挙動の新たなシグネチャを定義する必要があるBaleenの中間的な質問生成ステップがあります: 欠けている情報を特定するための検索クエリーを生成するためのいくつかのコンテキストと質問を入力します。

```py
class GenerateSearchQuery(dspy.Signature):
    """複雑な質問への回答に役立つシンプルな検索クエリーを記述"""

    context = dspy.InputField(desc="may contain relevant facts")
    question = dspy.InputField()
    query = dspy.OutputField()
```

:::note
**情報**
コンテキストフィールドに対する説明の重複を避けるために`context = GenerateAnswer.signature.context`と記述しても構いません。
:::

必要なシグネチャの準備ができたので、Baleenパイプラインの構築をスタートしましょう！

# パイプラインの構築

それでは、プログラム自身である`SimplifiedBaleen`を定義しましょう。これを実装する方法はいくつかありますが、このバージョンはキーとなる要素に限定します。

```py
from dsp.utils import deduplicate

class SimplifiedBaleen(dspy.Module):
    def __init__(self, passages_per_hop=3, max_hops=2):
        super().__init__()

        self.generate_query = [dspy.ChainOfThought(GenerateSearchQuery) for _ in range(max_hops)]
        self.retrieve = dspy.Retrieve(k=passages_per_hop)
        self.generate_answer = dspy.ChainOfThought(GenerateAnswer)
        self.max_hops = max_hops
    
    def forward(self, question):
        context = []
        
        for hop in range(self.max_hops):
            query = self.generate_query[hop](context=context, question=question).query
            passages = self.retrieve(query).passages
            context = deduplicate(context + passages)

        pred = self.generate_answer(context=context, question=question)
        return dspy.Prediction(context=context, answer=pred.answer)
```

見てわかるように、`__init__`メソッドではいくつかのサブモジュールを定義します:

- **generate_query:** それぞれのホップに対して、`GenerateSearchQuery`シグネチャの1つの`dspy.ChainOfThought`のpredictorを持つことになります。
- **retrieve:** このモジュールは、`dspy.Retrieve`モジュール経由で、定義済みのColBERT RM検索インデックスに対して生成されたクエリーを用いて検索を実行します。
- **generate_answer:** この`dspy.Predict`モジュールは最終回答を生成するための`GenerateAnswer`シグネチャで使用されます。

`forward`メソッドは、シンプルなコントロールフローでこれらのサブモジュールを使用します。

1. 最初、`self.max_hops`の回数ループを行います。
1. それぞれのイテレーションでは、`self.generate_query[hop]`でpredictorを用いた検索クエリーを生成します。
1. そのクエリーを用いてtop-kの文章を収集します。
1. `context` accumulatorに(重複排除した)文章を追加します。
1. ループの後で、回答を生成するために`self.generate_answer`を使用します。
1. 収集された`context`と推定した`answer`を用いた推定結果を返却します。

# パイプラインの実行

zero-shot(コンパイルなし)の設定でこのプログラムを実行しましょう。

これは必ずしもパフォーマンスが悪いことを意味しませんが、むしろ、最小限の指示からサブタスクを理解するために背後のLMの信頼性によって、直接的なボトルネックの影響を受けます。多くの場合、最も簡単で標準的なタスク(一般的なエンティティに対するシンプルな質問への回答)で、最も効果でパワフルなモデル(GPT-4など)を用いる際には問題になりません。

```py
# このシンプルなRAGプログラムにお好きな質問をします
my_question = "How many storeys are in the castle that David Gregory inherited?"

# 推定結果を取得します。これには `pred.context` と `pred.answer` が含まれます
uncompiled_baleen = SimplifiedBaleen()  # コンパイルなしのプログラム (zero-shot)
pred = uncompiled_baleen(my_question)

# コンテキストと解答の表示
print(f"Question: {my_question}")
print(f"Predicted Answer: {pred.answer}")
print(f"Retrieved Contexts (truncated): {[c[:200] + '...' for c in pred.context]}")
```

> Question: How many storeys are in the castle that David Gregory inherited?
> Predicted Answer: Five
> Retrieved Contexts (truncated): ['David Gregory (physician) | David Gregory (20 December 1625 – 1720) was a Scottish physician and inventor. His surname is sometimes spelt as Gregorie, the original Scottish spelling. He inherited Kinn...', 'David Webster (architect) | David Webster (1885–1952) was a Scottish-Canadian architect best known for his designs of elementary schools in Saskatoon, Saskatchewan, Canada. His school designs were oft...', 'David Gregory (footballer, born 1970) | Born in Polstead, Gregory began his career at Ipswich Town, making 32 appearances between 1987–1995. He made two appearances on loan at Hereford United and thre...', 'Kinnairdy Castle | Kinnairdy Castle is a tower house, having five storeys and a garret, two miles south of Aberchirder, Aberdeenshire, Scotland. The alternative name is Old Kinnairdy....', 'Kinnaird Castle, Brechin | Kinnaird Castle is a 15th-century castle in Angus, Scotland. The castle has been home to the Carnegie family, the Earl of Southesk, for more than 600 years....', 'Kinnaird Head | Kinnaird Head (Scottish Gaelic: "An Ceann Àrd" , "high headland") is a headland projecting into the North Sea, within the town of Fraserburgh, Aberdeenshire on the east coast of Scotla...']

以下を用いてLMに対する最新の**3つ**の呼び出しを調査することができます(最初のhopのクエリーの生成、2つ目のhopのクエリーの生成、回答の生成):

```py
turbo.inspect_history(n=3)
```

```



複雑な質問への回答に役立つシンプルな検索クエリーを記述

---

Follow the following format.

Context: may contain relevant facts

Question: ${question}

Reasoning: Let's think step by step in order to ${produce the query}. We ...

Query: ${query}

---

Context: N/A

Question: How many storeys are in the castle that David Gregory inherited?

Reasoning: Let's think step by step in order to produce the query. We need to find information about the castle that David Gregory inherited, specifically the number of storeys it has.

Query: "David Gregory inherited castle storeys"





複雑な質問への回答に役立つシンプルな検索クエリーを記述

---

Follow the following format.

Context: may contain relevant facts

Question: ${question}

Reasoning: Let's think step by step in order to ${produce the query}. We ...

Query: ${query}

---

Context:
[1] «David Gregory (physician) | David Gregory (20 December 1625 – 1720) was a Scottish physician and inventor. His surname is sometimes spelt as Gregorie, the original Scottish spelling. He inherited Kinnairdy Castle in 1664. Three of his twenty-nine children became mathematics professors. He is credited with inventing a military cannon that Isaac Newton described as "being destructive to the human species". Copies and details of the model no longer exist. Gregory's use of a barometer to predict farming-related weather conditions led him to be accused of witchcraft by Presbyterian ministers from Aberdeen, although he was never convicted.»
[2] «David Webster (architect) | David Webster (1885–1952) was a Scottish-Canadian architect best known for his designs of elementary schools in Saskatoon, Saskatchewan, Canada. His school designs were often in a Collegiate Gothic style emphasizing a central tower, locally referred to as a "castle style". Along with other local architects of his era, such as Walter LaChance and Storey and Van Egmond, Webster prospered during the province’s 1912 economic boom which sparked a frenzy of new construction.»
[3] «David Gregory (footballer, born 1970) | Born in Polstead, Gregory began his career at Ipswich Town, making 32 appearances between 1987–1995. He made two appearances on loan at Hereford United and three appearances at Peterborough United after leaving Ipswich. He joined Colchester United in 1995 and spent seven years at Layer Road, making 226 league appearances and scoring 19 goals. Gregory helped the U's to their first league promotion for 22 years in 1998 when he stepped up to put Colchester ahead from the spot in the Third Division playoff final. Gregory made history along with Neil as the first pair of brothers in a play-off final in the same match. His brother Neil joined the U's in 1998 and played alongside David until 2000. Gregory featured regularly in the first team until he cracked a bone in his foot playing against Port Vale in March 2002 and failed to recover fitness by the summer break. He again teamed up with his brother Neil at Canvey Island in July 2002.»

Question: How many storeys are in the castle that David Gregory inherited?

Reasoning: Let's think step by step in order to produce the query. We know that David Gregory inherited Kinnairdy Castle in 1664. To find out how many storeys the castle has, we need to search for information on the architectural details of Kinnairdy Castle.

Query: Kinnairdy Castle architecture details





簡潔な事実に基づく回答で質疑応答

---

Follow the following format.

Context: may contain relevant facts

Question: ${question}

Reasoning: Let's think step by step in order to ${produce the answer}. We ...

Answer: often between 1 and 5 words

---

Context:
[1] «David Gregory (physician) | David Gregory (20 December 1625 – 1720) was a Scottish physician and inventor. His surname is sometimes spelt as Gregorie, the original Scottish spelling. He inherited Kinnairdy Castle in 1664. Three of his twenty-nine children became mathematics professors. He is credited with inventing a military cannon that Isaac Newton described as "being destructive to the human species". Copies and details of the model no longer exist. Gregory's use of a barometer to predict farming-related weather conditions led him to be accused of witchcraft by Presbyterian ministers from Aberdeen, although he was never convicted.»
[2] «David Webster (architect) | David Webster (1885–1952) was a Scottish-Canadian architect best known for his designs of elementary schools in Saskatoon, Saskatchewan, Canada. His school designs were often in a Collegiate Gothic style emphasizing a central tower, locally referred to as a "castle style". Along with other local architects of his era, such as Walter LaChance and Storey and Van Egmond, Webster prospered during the province’s 1912 economic boom which sparked a frenzy of new construction.»
[3] «David Gregory (footballer, born 1970) | Born in Polstead, Gregory began his career at Ipswich Town, making 32 appearances between 1987–1995. He made two appearances on loan at Hereford United and three appearances at Peterborough United after leaving Ipswich. He joined Colchester United in 1995 and spent seven years at Layer Road, making 226 league appearances and scoring 19 goals. Gregory helped the U's to their first league promotion for 22 years in 1998 when he stepped up to put Colchester ahead from the spot in the Third Division playoff final. Gregory made history along with Neil as the first pair of brothers in a play-off final in the same match. His brother Neil joined the U's in 1998 and played alongside David until 2000. Gregory featured regularly in the first team until he cracked a bone in his foot playing against Port Vale in March 2002 and failed to recover fitness by the summer break. He again teamed up with his brother Neil at Canvey Island in July 2002.»
[4] «Kinnairdy Castle | Kinnairdy Castle is a tower house, having five storeys and a garret, two miles south of Aberchirder, Aberdeenshire, Scotland. The alternative name is Old Kinnairdy.»
[5] «Kinnaird Castle, Brechin | Kinnaird Castle is a 15th-century castle in Angus, Scotland. The castle has been home to the Carnegie family, the Earl of Southesk, for more than 600 years.»
[6] «Kinnaird Head | Kinnaird Head (Scottish Gaelic: "An Ceann Àrd" , "high headland") is a headland projecting into the North Sea, within the town of Fraserburgh, Aberdeenshire on the east coast of Scotland. The 16th-century Kinnaird Castle was converted in 1787 for use as the Kinnaird Head Lighthouse, the first lighthouse in Scotland to be lit by the Commissioners of Northern Lights. Kinnaird Castle and the nearby Winetower were described by W. Douglas Simpson as two of the nine castles of the Knuckle, referring to the rocky headland of north-east Aberdeenshire. Both buildings are category A listed buildings.»

Question: How many storeys are in the castle that David Gregory inherited?

Reasoning: Let's think step by step in order to produce the answer. We know from the context that David Gregory inherited Kinnairdy Castle. According to the information provided, Kinnairdy Castle has five storeys.

Answer: Five



```


# パイプラインの最適化

しかし、zero-shotアプローチは、より特化したタスク、新しいドメイン/環境、より効率的な(あるいはオープンな)モデルではすぐに不十分になります。

これに対応するために、**DSPy**はコンピレーションを提供します。我々のマルチホッププログラム(`SimplifiedBaleen`)をコンパイルしましょう。

コンピレーションのための検証ロジックを最初に定義しましょう:

- ゴールドアンサーにマッチする推定された回答
- ゴールドアンサーを含む収集コンテキスト
- 生成されたクエリーのいずれも無闇に大きくなっていないこと(いずれも100文字長を超えていないこと)
- 生成されたクエリーのいずれも適当に繰り返されていないこと(いずれも以前のクエリーの0.8以上のF1スコアになっていること)

```py
def validate_context_and_answer_and_hops(example, pred, trace=None):
    if not dspy.evaluate.answer_exact_match(example, pred): return False
    if not dspy.evaluate.answer_passage_match(example, pred): return False

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

ここでは、few-shotの例でパイプラインのpredictorを最適化するために、**DSPy**の最も基本的なテレプロンプターである`BootstrapFewShot`を活用します。

```py
from dspy.teleprompt import BootstrapFewShot

teleprompter = BootstrapFewShot(metric=validate_context_and_answer_and_hops)
compiled_baleen = teleprompter.compile(SimplifiedBaleen(), teacher=SimplifiedBaleen(passages_per_hop=2), trainset=trainset)
```

# パイプラインの評価

それでは、評価関数を定義し、コンパイルありなしのBaleenパイプラインのパフォーマンスを比較しましょう。このdevdetは完全に信頼できるベンチマークとしては寄与しませんが、このチュートリアルで用いるのには役立ちます。

```py
from dspy.evaluate.evaluate import Evaluate

# 適切なドキュンメントを検索したのかどうかをチェックするメトリックの定義
def gold_passages_retrieved(example, pred, trace=None):
    gold_titles = set(map(dspy.evaluate.normalize_text, example["gold_titles"]))
    found_titles = set(
        map(dspy.evaluate.normalize_text, [c.split(" | ")[0] for c in pred.context])
    )
    return gold_titles.issubset(found_titles)

# `evaluate_on_hotpotqa` 関数のセットアップ。以下でこの関数を複数回使います。
evaluate_on_hotpotqa = Evaluate(devset=devset, num_threads=1, display_progress=True, display_table=5)

uncompiled_baleen_retrieval_score = evaluate_on_hotpotqa(uncompiled_baleen, metric=gold_passages_retrieved)

compiled_baleen_retrieval_score = evaluate_on_hotpotqa(compiled_baleen, metric=gold_passages_retrieved)

print(f"## Retrieval Score for uncompiled Baleen: {uncompiled_baleen_retrieval_score}")
print(f"## Retrieval Score for compiled Baleen: {compiled_baleen_retrieval_score}")
```

```
## Retrieval Score for uncompiled Baleen: 42.0
## Retrieval Score for compiled Baleen: 54.0
```

すばらしい！このコンパイルされたマルチホップのプログラムには何かあるのかもしれません。

以前、我々はシンプルなプログラムはそれぞれの質問に回答するためにに必要なすべての根拠を探し出すことに関して、非常に効果的ではないと言及しました。`SimplifiedBaleen`のforward関数でいくつかの優れたプロンプト技術を追加することで、これは解決されるのでしょうか？プログラムのコンパイルはパフォーマンスを改善するのでしょうか？

このチュートリアルでは我々の知見をデモンストレーションしましたが、これらの質問に対する回答は常に明白なわけではありません。しかし、DSPyは最小限の工数で、さまざまなアプローチを容易にトライできるようにしてくれます。

ここではシンプルですがパワフルなパイプラインの構築例を見てきましたので、ぜひご自身のものを作ってみてください！

### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)