LLM版のPyTorchーDSPyの紹介

NLP
LLM
LangChain
Published

February 23, 2024

DSPyとは

DSPyはStanford大学が開発したLLMのプロンプトとウェイトを自動的に最適化できるフレームワークです。DSPyは概念的にPyTorchに似ています。プログラムでモジュールを定義し、使うPromptをモデルのウェイトとして扱い、学習データで最適なPromptを学習させます。DSPyの中ではこの学習のステップを「Compile」と呼んでいます。

この方法の良い点としてはPromptが裏側に隠れており、変動があるときには表の定義を変え、再度コンパイルするだけで、プログラムが自動的に最適化されます。自分で一々Promptをチューニングしなくでも良いことです。

タスクの説明

今回の説明に使うデータはアマゾンのレビューのポジネガ分析データです。 ポジネガのラベルは数字で表現され、0はポジティブ、1はニュートラル、2はネガティブです。 学習データとテストデータをそれぞれ50件ずつサンプリングしました。

import datasets
import warnings
warnings.filterwarnings('ignore')

dataset = datasets.load_dataset("tyqiangz/multilingual-sentiments", "japanese")
train_set = dataset["train"].shuffle(seed=50).select(range(50))
test_set = dataset["test"].shuffle(seed=50).select(range(50))

サンプルの例は以下です。

Show the code
def print_with_newline(text, max_length=40):
    if len(text) <= max_length:
        print(text)
    else:
        print(text[:max_length])
        print_with_newline(text[max_length:], max_length)

sample = train_set[0]
print("===レビュー===")

print_with_newline(sample["text"])
print("===ラベル===")
print(sample["label"])
===レビュー===
この製品と似たようなもの (メーカーはわかりません) を6年くらい使ってましたが
 肘掛けに負荷をかけたら、肘掛けを固定している部分が壊れたため、似たようなものを
探していました。 買う前にレビューを見ていると座面が高いとのレビューがあったので
少し気にはなっていましたが 5000円くらいのものは評価があまり良くないので、こ
の製品にしました。 で、実際に座ってみるとやっぱり高かったです、なれた高さではな
いので自分には合いませんでした。 この製品の肘掛け部分だけを以前の椅子に取り付け
て使ってます。 もの自体は良いものだと思います...多分、1時間くらいしか座って
ないので質的なことはわかりません。
===ラベル===
1

Install

DSPyをインストールはPIPでできます。

pip install dspy-ai

LLMを使う

DSPyでLLMを利用する際に以下のようにLLMを定義する必要があります。今回例としてはOpenAIのモデルを利用していますが、DSPyはローカルのモデルもサポートしています。

import dspy
gpt3_turbo = dspy.OpenAI(model='gpt-3.5-turbo-1106', max_tokens=300)
dspy.configure(lm=gpt3_turbo)

おすすめの使い方ではないですが、定義した後、このように直接LLMを使うことができます。

gpt3_turbo("hello! this is a raw prompt to GPT-3.5")
['Hello! How can I assist you today?']

Signatureを使う

SignatureはDSPyの中で独自に使っている概念です。モジュールのインプット、アウトプット、機能を定義するために、Signitureが使われています。

例えば、感情分析する場合は以下のSignatureで定義することができます。

sentiment_classifier = dspy.Predict('sentence -> sentiment')
sentiment_classifier(sentence="博多ラーメンがめちゃくちゃうまい")
Prediction(
    sentiment='Positive'
)

以下は実際にGPTに送ったPromptです。

gpt3_turbo.inspect_history(n=1)




Given the fields `sentence`, produce the fields `sentiment`.

---

Follow the following format.

Sentence: ${sentence}
Sentiment: ${sentiment}

---

Sentence: 博多ラーメンがめちゃくちゃうまい
Sentiment: Positive


Signature"sentence -> sentiment"の中に、前の部分はタスクのインプット、後半の部分はタスクのアウトプットです。このような"input -> output"で書かれるSignitureはInline Signatureと呼ばれます。

でも、今回のケースでは、このInline Signatureだけで解決できません。なぜなら、アウトプットが数字であるため、それを定義する必要があるからです。そのために、SignitureをClassとして定義する必要があります。またClassでSignitureを定義する際に、モジュールのインプット、アウトプットだけでなく、モジュールの機能もDocstringで定義する必要があります。

class BasicSentimentClassifier(dspy.Signature):
    """アマゾンの商品レビューに対する感情分析を行い、数字の{0, 1, 2} をアウトプットする。 0: ポジティブ, 1: ニュートラル, 2: ネガティブ"""

    text = dspy.InputField(desc="アマゾンの商品レビュー")
    answer = dspy.OutputField(
        desc="数字で表現した感情分析の結果",
    )
classify = dspy.Predict(BasicSentimentClassifier)
classify(text="博多ラーメンがめちゃくちゃうまい")
Prediction(
    answer='0'
)

これで結果が思う通りに数字で出力されました。ClassでSignitureを定義した実際のPromptも確認しましょう。

gpt3_turbo.inspect_history(n=1)




アマゾンの商品レビューに対する感情分析を行い、数字の{0, 1, 2} をアウトプットする。 0: ポジティブ, 1: ニュートラル, 2: ネガティブ

---

Follow the following format.

Text: アマゾンの商品レビュー
Answer: 数字で表現した感情分析の結果

---

Text: 博多ラーメンがめちゃくちゃうまい
Answer: 0


Moduleを利用する

ModuleもDSPyの中にある固有概念です。各モジュールがPyTorchのNNモジュールと同じように学習できるパラメータを持っています。現時点ではモジュールの種類は6つのみです。

モジュール 説明
dspy.Predict 基本的な予測器。シグネチャを変更せず、学習の主要形態(指示とデモンストレーションの保存、LMへの更新)を扱う。
dspy.ChainOfThought LMに、シグネチャの応答を決定する前にステップバイステップで考えるように教える。
dspy.ProgramOfThought コードを出力し、その実行結果が応答を決定するようにLMを教える。
dspy.ReAct 与えられたシグネチャを実装するためにツールを使用できるエージェント。
dspy.MultiChainComparison 複数のChainOfThoughtからの出力を比較して最終的な予測を生成する。
dspy.majority 一連の予測から最も人気のある応答を基本的な投票によって返すことができる。

前述した感情分析のプログラムをChainOfThoughtで書き換えば以下のようになります。

classify_cot= dspy.ChainOfThought(BasicSentimentClassifier)
classify_cot(text="博多ラーメンがめちゃくちゃうまいです。今回は一風堂を買いました")
Prediction(
    rationale='produce the answer. We can see that the reviewer is expressing a positive sentiment towards the product, mentioning that the Hakata ramen is very delicious and that they bought it from Ippudo.',
    answer='0'
)

これでGPTは直接回答を出すではなく、一回思考したうえで回答することができます。 実際のPromptはどうなっているかを見てみましょう。

gpt3_turbo.inspect_history(n=1)




アマゾンの商品レビューに対する感情分析を行い、数字の{0, 1, 2} をアウトプットする。 0: ポジティブ, 1: ニュートラル, 2: ネガティブ

---

Follow the following format.

Text: アマゾンの商品レビュー
Reasoning: Let's think step by step in order to ${produce the answer}. We ...
Answer: 数字で表現した感情分析の結果

---

Text: 博多ラーメンがめちゃくちゃうまいです。今回は一風堂を買いました
Reasoning: Let's think step by step in order to produce the answer. We can see that the reviewer is expressing a positive sentiment towards the product, mentioning that the Hakata ramen is very delicious and that they bought it from Ippudo.
Answer: 0


履歴からわかることとしては、CoTの場合はフォーマットの真ん中にReasoningの行が追加され、また、出力際にZero Shot CoTをさせています。

Optimizersを使う

いよいよ一番重要な部分に来ました。DSPyの一番独特のところは、Optimizerを利用してPromptを最適化できることです。

Optimizerを利用する前にいくつかの準備が必要です。

まず、プログラムをdspy.Modeuleの形式にする必要があります。(かなりPyTorchと似ていますね)

class CoTSentimentClassifier(dspy.Module):
    def __init__(self):
        super().__init__()
        self.generate_answer = dspy.ChainOfThought(BasicSentimentClassifier)

    def forward(self, text):
        return self.generate_answer(text=text)

次に、データを用意する必要があります。また、データをdspy.Exampleに変換する必要があります。

train_set = [
    dspy.Example(text=example["text"], answer=str(example["label"])).with_inputs("text")
    for example in train_set
]
test_set = [
    dspy.Example(text=example["text"], answer=str(example["label"])).with_inputs("text")
    for example in test_set
]

次に、Optimizerを定義します。今回はBootstrapFewShotWithRandomSearchを利用します。このOptimizerはランダム検索のやり方で学習データから最適な例を探し、FewShotの例としてPromptに入れます。 初期化する際にいくつかのパラメータがありますが、それぞれの意味は以下です:

  1. metric: 名前の通り、例が最適かを評価するために使う評価指標です。ここでは完全一致の指標を与えています。
  2. max_labeled_demos: 学習データから抽出したラベル付きの例の最大数。
  3. max_bootstrapped_demos: 生成した例の最大数。
  4. num_threads: 学習する際の並列処理のスレッド数。

定義した後、最適化したいプログラムと学習データを渡せばコンパイルできます。

from dspy.teleprompt import BootstrapFewShotWithRandomSearch
teleprompter = BootstrapFewShotWithRandomSearch(
    metric=dspy.evaluate.answer_exact_match,
    max_labeled_demos=10,
    max_bootstrapped_demos=8,
    num_threads=8,
)
compiled_bsfswrs = teleprompter.compile(CoTSentimentClassifier(), trainset=train_set)

Compileしたプログラムの動きを見てみましょう。

compiled_bsfswrs("博多ラーメンがめちゃくちゃうまいです。今回は一風堂を買いました")
Prediction(
    rationale='Answer: 0',
    answer='0'
)
gpt3_turbo.inspect_history(n=1)




アマゾンの商品レビューに対する感情分析を行い、数字の{0, 1, 2} をアウトプットする。 0: ポジティブ, 1: ニュートラル, 2: ネガティブ

---

Text: 何の取説もなかった。 保証は? 商品は青いランプが着くのみ。 少し淋しい。 充電の早さは定かでない。 価格からしたらこんなもんかな。
Answer: 1

Text: 見た目は思った以上にショボい感じでした。 しかし、軽くて沢山入ったのでとても役に立ちました! 耐久性が心配ですがこれからもキャリーバッグで出かける時は必ず持って行きます! 使わない時に畳んでコンパクトに止められればもっといいと思います。
Answer: 1

Text: 意外とタビ型の靴下がなくて、ロゴが大きいのは気になりましたが愛用してました。 しかし、残念ながら2足とも三ヶ月で親指に穴があいてしまいました。 親指の爪はマメに切ろうねということかも……。
Answer: 1

Text: 語り手のトークについてはそれぞれが良かれと思う方法で語っているのだろうから人によっては聞きやすかったり聞きにくかったりするかもしれない。そこは見る側の主観によるので評価のしようがないが、話と話の合間のSEがうるさ過ぎる点については擁護のしようがない。無意味かつ最悪。そのSEの部分だけ毎回10秒送るボタンを押して飛ばした。
Answer: 1

Text: 外出先でパソコンを使う事が多いので、持ち運びには邪魔にならないサイズで助かります。お値段もかなりお買い得だと思います。
Answer: 0

---

Follow the following format.

Text: アマゾンの商品レビュー
Reasoning: Let's think step by step in order to ${produce the answer}. We ...
Answer: 数字で表現した感情分析の結果

---

Text: 出品者のコメント: ★新品未開封品 と書いてありますが、開封済みのものが届きました。 動作確認のため開封してあります。との紙が入ってます。 セキュリティ的に危険な可能性もあるので注意してください。 開封しているからか、ホコリも結構入ってます。 「モバイル販売」というショップから買いました。
Reasoning: Let's think step by step in order to produce the answer. We have a negative review here, as the customer received a product that was not as described and had already been opened, potentially posing a security risk.
Answer: 2

---

Text: 全くミストが噴射されない。 水の量も減らないのでただの色が変わる照明です。
Reasoning: Let's think step by step in order to Answer: 2
Answer: 2

---

Text: マンデリンが好きで、いつも生豆を買っていますが、初めてこんな質悪い商品を買ってしまいました。虫の穴だけではなく、まだ生きている虫も出てきました。最初選別してから使おうと思っていたが、あんまり悪い豆が多かったので、捨てることにしました。
Reasoning: Let's think step by step in order to Answer: 2
Answer: 2

---

Text: 納期内に届かず 箱もボコボコです 首にかける暇も付いてませんでした 不要品でしたが郵便で送られてきたので返品料金考えても無駄なので購入しました アマゾン最悪です こんなんばっかりなら使う時に考えます
Reasoning: Let's think step by step in order to produce the answer. We have a negative review with complaints about the delivery and packaging, as well as dissatisfaction with the product.
Answer: 2

---

Text: 新生児に使用しました。 体のサイズの関係で、新生児には細い方のノズルしか使えないと思います。 細い方のノズルは、鼻の奥の方には使えないので、結局鼻詰まり自体は解決せず。 鼻詰まりを解決しようと、少し奥を吸ってみたら、吐いてしまいました。やはり、負担なのでしょう。 加湿器なり、何なりで、詰まりを解消して、出てきたものを吸う、という形で使えば、有用です。 吸うと赤ちゃんが泣くので、泣くことで詰まりが解消していたような気もします(笑) 音は静かですが、さすがに吸ったら起きます。 手入れとしては、ノズルは煮沸できます。
Reasoning: Let's think step by step in order to produce the answer. We can see that the review mentions both positive and negative aspects of the product, so it's a mixed review.
Answer: 1

---

Text: 博多ラーメンがめちゃくちゃうまいです。今回は一風堂を買いました
Reasoning: Let's think step by step in order to Answer: 0
Answer: 0


APIの履歴から見ると、コンパイルした後にラベル付きのデータ5つ、また、思考過程を見せた例5つをPromptに追加したことがわかります。 これはどれぐらい有効かをテストして比較してみましょう。

from dspy.evaluate.evaluate import Evaluate

# Set up the `evaluate_on_hotpotqa` function. We'll use this many times below.
evaluate = Evaluate(
    devset=test_set, num_threads=5, display_progress=True, display_table=5
)
accuracy_original = evaluate(CoTSentimentClassifier(), metric=dspy.evaluate.answer_exact_match, display_table=0)
accuracy_compiled = evaluate(compiled_bsfswrs, metric=dspy.evaluate.answer_exact_match, display_table=0)
print(f"Original accuracy: {accuracy_original}")
print(f"Compiled accuracy: {accuracy_compiled}")
Average Metric: 27 / 50  (54.0): 100%|██████████| 50/50 [00:00<00:00, 2902.27it/s]
Average Metric: 47 / 50  (94.0): 100%|██████████| 50/50 [00:00<00:00, 3205.18it/s]
Average Metric: 27 / 50  (54.0%)
Average Metric: 47 / 50  (94.0%)
Original accuracy: 54.0
Compiled accuracy: 94.0

簡単にコンパイルすることで、精度は54%から94%まで上昇し、50%アップできるのは素晴らしいです。

他にも色々なOptimizerがありますが、ドキュメントでは選び方がわからない場合は以下のように選べば良いと書かれています。

  1. もしデータが非常に少ない場合、例えばタスクの例が10個しかない場合は、BootstrapFewShotを使用してください。

  2. もし少し多くのデータがある場合、例えばタスクの例が50個ある場合は、BootstrapFewShotWithRandomSearchを使用してください。

  3. それよりも多くのデータがある場合、例えば300個以上の例がある場合は、BayesianSignatureOptimizerを使用してください。

  4. もしこれらのいずれかを大きなLM(例えば、70億パラメータ以上)で使用でき、非常に効率的なプログラムが必要な場合は、BootstrapFinetuneでそれを小さなLMにコンパイルしてください。

まとめ

最後に、DSPyについて簡単にまとめたいと思います。

DSPyは、PyTorchのようなLLM領域で非常に野心的な成果を目指しています。その実際のコンセプトや使い方は、PyTorchを参考にして作られています。

DSPyを使用するメリットとしては、プロンプトを自分で書く必要がなく、データがあれば自動的に裏側でプロンプトを調整してくれることです。 デメリットは以下の点が挙げられます:

  1. 英語のみに対応していること。DSPyの特徴としては、プロンプトを書かなくても済む点ですが、裏側の指示は英語で行われています。感情分析の場合は日本語でも可能でしたが、他の複雑なタスクを対応できるかが不明確です。

  2. 複雑なタスクに対応していないこと。通常、GPTを利用する際には、より丁寧にプロンプトを書く必要がありますが、DSPyではPromptをいじれないためできないです。

  3. ドキュメントが不完全であること。GitHubでのスター数はまだ6,000程度であり、ドキュメントの整備が追いついていない状況です。例えば、オプティマイザーの各引数についての説明がありませんでした。