Closed9

テキスト最適化のためのLLM「勾配」パイプラインを構築できる「TextGrad」を試す

kun432kun432

公式サイト

https://textgrad.com/

GitHubレポジトリ

https://github.com/zou-group/textgrad

TextGrad: テキストによる自動「微分」

テキストのグラデーションのためのオートグラデーションエンジン!

TextGradは、テキストによる自動「微分」を構築する強力なフレームワークです。TextGradは、LLMが提供するテキストフィードバックを通じてバックプロパゲーションを実装し、グラディエントメタファーを強力に構築します。

私たちは、独自の損失関数を定義し、テキストフィードバックを使用して最適化できる、シンプルで直感的なAPIを提供しています。このAPIはPytorch APIに類似しており、あなたの用途に簡単に適応させることができます。


referred from https://github.com/zou-group/textgrad

kun432kun432

論文

https://arxiv.org/abs/2406.07496

NotebookLMまとめ

概要

この論文では、TEXTGRADというフレームワークを紹介します。これは、大規模言語モデル(LLM)が提供するテキストベースのフィードバックを利用して、複雑なAIシステムを最適化する、自動的な「微分」のための強力な方法です。TEXTGRADは、LLMからの自然言語フィードバックを「テキスト勾配」として解釈し、AIシステム内の個々のコンポーネントを改善するために、これらの勾配を伝播させます。このフレームワークは、従来の自動微分の概念を拡張したものであり、PyTorchのような既存の深層学習フレームワークの抽象化と構文を共有しています。TEXTGRADは、コーディング問題の解決から、分子の最適化、放射線療法の治療計画の改善まで、さまざまな分野でその有効性を示しています。この論文は、TEXTGRADの仕組み、実装の詳細、およびさまざまなアプリケーションにおけるその成功事例について説明しています。

落合プロンプト

1. どんなもの?

TEXTGRADは、複合AIシステム、つまり複数の大規模言語モデル(LLM)やその他の複雑なコンポーネントを組み合わせたシステムを最適化するための新しいフレームワークです。 従来のニューラルネットワークにおける誤差逆伝播法自動微分のように、TEXTGRADはLLMからのテキストフィードバックを用いて、複合AIシステムの個々のコンポーネントを改善します。 TEXTGRADは、コードスニペットから分子構造まで、計算グラフ内の変数を最適化するための、豊かで一般的な自然言語による提案をLLMから提供します。

2. 先行研究を比べてどこがすごい?

先行研究であるDSPyProTeGiは、主にプロンプト最適化に焦点を当てていました。 TEXTGRADは、プロンプト最適化だけでなく、インスタンス最適化、つまりコードスニペット、問題の解、分子などのインスタンス自体を最適化する点で大きく進歩しています。 これは、質問応答、分子設計、放射線治療計画の最適化など、多様なアプリケーションを通じて実証されています。

3. 技術や手法の肝はどこ?

TEXTGRADは、複合AIシステムを計算グラフとして表現します。 各変数は、LLM API呼び出し、シミュレータ、外部数値ソルバーなどの複雑な(必ずしも微分可能ではない)関数呼び出しの入力と出力となります。 LLMは、システムを改善するために変数をどのように変更すべきかを記述する、有益で解釈可能な自然言語のフィードバック(テキスト勾配)を各変数に提供します。 これらのテキスト勾配は、任意の関数を介して逆伝播されます。

4. どうやって有効だと検証した?

TEXTGRADの有効性は、以下の多様なアプリケーションで検証されています。

  • コーディング: LeetCodeの難しいコーディング問題の解を最適化し、gpt-4oと既存の最良手法の性能を相対的に20%向上させました。
  • 問題解決: Google-Proof Question Answeringベンチマークにおいて、GPT-4oのゼロショット精度を51%から55%に向上させました。
  • 推論: いくつかの推論タスクでGPT-3.5の性能をGPT-4に近づけるようにプロンプトを最適化しました。
  • 化学: 望ましい薬物類似性とin silico結合親和性を持つ新しい小分子を設計しました。
  • 医療: 前立腺がん患者の放射線治療計画を最適化し、望ましい標的線量を達成し、副作用を軽減しました。

5. 議論はある?

TEXTGRADは、LLMを活用した自動微分フレームワークの可能性を実現するための第一歩ですが、いくつかの制限事項があり、将来の研究課題となっています。

  • 適用範囲の拡大:科学的発見における反復的なプロセスを加速し、エンジニアリングの努力の生産性を向上させるために、ツール使用や検索拡張生成システムなどの、より多くのコンポーネントを含むように計算グラフの操作を拡張することが期待されています。
  • 最適化アルゴリズムの設計:数値最適化、自動微分、TEXTGRADの間には、多くの有益な関連性があり、分散低減技術、適応勾配、LLMを用いた自己検証などを利用して最適化の安定性を向上させることが興味深い方向性として考えられています。
  • メタ学習アプローチ:TextGrad自体などの方法を使用してTEXTGRADフレームワークを最適化するためのメタ学習アプローチも、興味深い将来の研究方向です。

6. 次に読むべき論文は?

TEXTGRADの基盤となった研究として、DSPyProTeGi が挙げられます。

  • DSPyは、複雑なLLMベースのシステムを潜在的に多くの層を持つプログラムとして捉え、プログラム的に構築および最適化する方法を提案した先駆的な研究です。
  • ProTeGiは、プロンプト最適化の文脈でテキスト勾配を定義した研究で、勾配はタスク中に発生した誤りに対してLLMから与えられる自然言語フィードバックです。

また、LLMを批評家またはオプティマイザとして使用する、より広範な研究分野も参考になります。 これらの研究は、LLMをオプティマイザとして使用する有用性を実証しており、TEXTGRADの将来の研究に役立つ洞察を提供する可能性があります。

ProTeGiは"Gradient Descent"というやつのことだね。

https://arxiv.org/abs/2305.03495

kun432kun432

PyTorchを理解している人ならば多分理解が早いのだろうと思う。自分はPyTorchを理解できていないし、雰囲気だけしかわからないけど、

  • LLMを使ったシステムを、イテレーションで改善するもの
  • DSPyやGradient Descentと違って、プロンプト「以外」も含めて改善する
  • ベースの考え方は"Gradient Descent"と同じで、「テキストベースの勾配降下法」を使うというもの。

と認識した。

で、PyTorchのコンポーネントや処理について、まずはいろいろ調べてみた。まだ全然わかってないけど、イメージとして料理に例えるとこうかなと。あっているのかどうかは全然自信がない。

とりあえずTextGradの各処理を理解するための最低限のイメージにはなるのではないかな、しらんけど。

kun432kun432

とりあえずQuick Startをやってみる。Colaboratoryで。

パッケージインストール

!pip install textgrad
!pip freeze | grep -i textgrad
textgrad==0.1.5

OpenAIのAPIキーをセットする。

import os
from google.colab import userdata

os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')

ではここからTextGradの使い方を見ていく。

import textgrad as tg

model = tg.BlackboxLLM("gpt-3.5-turbo")

question_string = """
25枚のシャツを天日干しで乾かすのに1時間かかるとしたら、
30枚のシャツを天日干しで乾かすにはどのくらい時間がかかりますか?
ステップバイステップで考えて
"""
question = tg.Variable(
    question_string,
    role_description="LLMに対する質問",
    requires_grad=False
)

answer = model(question)

INFO:textgrad:LLMCall function forward

まずtg.BlackboxLLMでLLMを定義する。これがPyTorchでいうところの「モデル」にあたる。

モデルに与える入力変数はtg.Variableを使って入力変数の役割とともに定義する。上記の場合はプロンプトになる。requires_gradパラメータは、Gradient Descentを使って最適化の対象とする場合にTrueを指定するが、Quick Startの例ではプロンプトは最適化の対象ではないのでFalseとなっている。

そして、モデルに入力変数を与えて、初期回答を得る。

初期回答は見てみる。

print(answer)

まず、1枚のシャツを天日干しで乾かすのにかかる時間を1時間とします。

  1. 25枚のシャツを乾かすのにかかる時間は、25枚 × 1時間 = 25時間です。

  2. 30枚のシャツを乾かすのにかかる時間を求めるために、1枚あたりの時間を求めます。
    30枚のシャツを乾かすのに必要な時間 = 25時間 × (30枚 ÷ 25枚) = 25時間 × 1.2 = 30時間

したがって、30枚のシャツを天日干しで乾かすのには、30時間かかります。

一般的に考えると、天日干しなのでスペースの問題がない限りは、何枚干しても同じ時間になるのが正解、ということで間違っている。

でここからTextGradによる回答の最適化を行って、正しい回答を得れるようにする。

# 逆伝播エンジンの設定
tg.set_backward_engine("gpt-4o", override=True)

# 回答に役割を定義する→回答も入力変数となる
answer.set_role_description("concise and accurate answer to the question")

# ステップ2: PyTorchと同様に、損失関数とオプティマイザーを定義します!
# ここではSGDは使用しませんが、代わりに「テキスト勾配」で動作するTGD
# (テキスト勾配降下法)を使用します。

# オプティマイザーの定義。回答を最適化対象のパラメータとして渡す
optimizer = tg.TGD(parameters=[answer])

# 損失関数の定義。回答を評価する。
evaluation_instruction = """
次の質問があります: {question_string}
この質問に対して与えられた回答を評価してください。
賢明で論理的そして非常に批判的に評価を行ってください。
簡潔なフィードバックのみを提供してください。
"""

# TextLossは、自然言語で指定できる損失関数であり、推論をどのように評価したいかを記述する。
loss_fn = tg.TextLoss(evaluation_instruction)

tg.set_backward_engine("gpt-4o", override=True)は最初に定義したモデルとは別の、評価・フィードバック・最適化に使うLLMの定義になる。

初期回答を最適化対象の入力変数とするために役割の定義を行い、これを対するオプティマイザーをtg.TGDで定義する。

回答を評価すしてフィードバックを行う損失関数は、tg.TextLossに評価用プロンプトを渡して定義する。

そして、この損失関数に初期回答を渡して損失を計算する。

loss = loss_fn(answer)

INFO:textgrad:LLMCall function forward

計算された損失は以下のようにテキストでのフィードバックという形で返ってくる。

print(loss.value)

回答には誤りがあります。シャツを天日干しで乾かす時間は、枚数に関係なく1枚あたり1時間です。したがって、25枚でも30枚でも、同時に干すことができれば、全てのシャツが1時間で乾きます。枚数が増えても、干すスペースが十分にある限り、乾かす時間は変わりません。したがって、30枚のシャツを乾かすのにかかる時間は1時間です。

このフィードバックを元にどう改善するかを分析し、その結果を元にオプティマイザで最適化する。

loss.backward()
optimizer.step()

INFO:textgrad:_backward_through_llm prompt
INFO:textgrad:_backward_through_llm gradient
INFO:textgrad:TextualGradientDescent prompt for update
INFO:textgrad:TextualGradientDescent optimizer response
INFO:textgrad:TextualGradientDescent updated text

最適化された回答を見てみる。

print(answer.value)

シャツを天日干しで乾かす時間は、1枚あたり1時間です。十分な干すスペースがある場合、25枚でも30枚でも、全てのシャツが1時間で乾きます。したがって、30枚のシャツを乾かすのにかかる時間は1時間です。

正しい回答が得られているのがわかる。

このあたりはPyTorchと全く同じ構文になっているらしい。

kun432kun432

で、Quick Startでなんとなく雰囲気はわかったものの、回答を最適化する、というのは、(個人的にはかもしれないけど)ユースケースとしてはイマイチピンとこない。

そこでプロンプトと回答を渡して、システムプロンプトを最適化する、というのをやってみようと思う。

以下の記事で取り上げたプロンプト改善エージェントの内容をTextGradでやってみる。

https://zenn.dev/kun432/scraps/91ed8647dbf2cb

なお、プロンプトなどは以下の記事を参考にさせていただいている。この場を借りて感謝したい。
https://zenn.dev/kazuwombat/articles/2095668882245d

以下のようなユーザとLLMのやりとりを実現するために、システムプロンプトを最適化するというのをやってみる。

System: ??????
User: こんにちは、あなたは誰?
Assitant: うちは大阪のおばちゃん、きよみやで〜。占いが得意やねん。なんでも答えるで〜。今日は何が聞きたいん?

import textgrad as tg

question_str = "こんにちは、あなたは誰?"
answer_str = "うちは大阪のおばちゃん、きよみやで〜。占いが得意やねん。なんでも答えるで〜。今日は何が聞きたいん?"

question = tg.Variable(
    question_str,
    role_description="LLMへの質問",
    requires_grad=False
)
answer = tg.Variable(
    answer_str,
    role_description="質問に対する回答",
    requires_grad=False
)

initial_system_prompt = "あなたは、東京に住んでいる薬剤師です。薬の質問に答えるのがあなたの仕事です。"
system_prompt = tg.Variable(
    initial_system_prompt,
    role_description="LLMがユーザからの質問に答えるためのシステムプロンプト",
    requires_grad=True,
)

llm_engine = tg.get_engine("gpt-4o-mini")
model = tg.BlackboxLLM(llm_engine, system_prompt=system_prompt)

ユーザの質問、それに対する理想となる回答、そして初期のシステムプロンプトをそれぞれ入力変数として定義する。ただし最適化対象はシステムプロンプトのみとなるため、システムプロンプトの入力変数にだけrequires_grad=Trueで定義する。

初期のシステムプロンプトは以下のように全然異なるものを指定している。

あなたは、東京に住んでいる薬剤師です。薬の質問に答えるのがあなたの仕事です。

では初回のLLMからの回答を得る

prediction = model(question)
print(prediction.value)

INFO:textgrad:LLMCall function forward

こんにちは!私は東京に住んでいる薬剤師です。薬に関する質問や相談があれば、お手伝いしますので、どうぞお気軽にお尋ねください。

初期プロンプトに基づいた回答が生成されており、理想の回答とは乖離しているのがわかる。

ではオプティマイザや損失関数を定義する。

tg.set_backward_engine("gpt-4o", override=True)

optimizer_system_prompt = """
あなたは大規模言語モデルのプロンプトエンジニアです。
大規模言語モデルを使ったチャットシステムのシステムプロンプトを、指摘された改善点に基づいて、修正するのがあなたの仕事です。
修正されたシステムプロンプトは、言語モデルに対する「指示」である必要があります。言語モデルが実際のチャットの応答で返すような会話口調の表現は使わずに、客観的に書いてください。
"""

optimizer = tg.TGD(parameters=[system_prompt], optimizer_system_prompt=optimizer_system_prompt)

evaluation_instruction = f"""
質問: {question_string}
理想の回答: {answer_str}
現在の回答: {prediction}

現在の回答と理想の回答を比較して、現在の出力が理想の出力に近づくための改善点をリストアップしてください。
"""

loss_fn = tg.TextLoss(evaluation_instruction)
loss = loss_fn(system_prompt)

INFO:textgrad:LLMCall function forward

Quick Startの例では損失関数だけに評価・フィードバック用のプロンプトを定義していたが、上記のようにオプティマイザにもプロンプトを設定することができる。これにより、フィードバックの結果を踏まえるだけでなく、そこに指示を加えることができる。

損失の内容≒フィードバックを見てみる。

理想の回答に近づけるための改善点を以下にリストアップします。

  1. キャラクター設定の変更: 現在の回答では「東京に住んでいる薬剤師」として自己紹介していますが、理想の回答では「大阪のおばちゃん、きよみ」として自己紹介しています。キャラクター設定を変更する必要があります。

  2. 口調の調整: 理想の回答では関西弁を使用しています。現在の回答は標準語で書かれているため、関西弁に変更する必要があります。

  3. 自己紹介の内容: 理想の回答では「占いが得意」と述べていますが、現在の回答では「薬剤師」としての自己紹介をしています。自己紹介の内容を占いに関連付ける必要があります。

  4. 質問の受け付け: 理想の回答では「今日は何が聞きたいん?」と親しみやすく質問を受け付けています。現在の回答も質問を受け付けていますが、口調を親しみやすく変更する必要があります。

  5. 回答のテーマ: 理想の回答は占いに関連したキャラクター設定ですが、現在の回答は薬に関する質問を受け付けています。テーマを占いに関連付ける必要があります。

これらの改善点を考慮して、理想の回答に近づけるように調整することができます。

ではこの内容を踏まえて、フィードバック分析・最適化する。

loss.backward()
optimizer.step()

最適化されたシステムプロンプトを見てみる。

print(system_prompt.value)

あなたは、大阪に住んでいるおばちゃん、きよみです。占いが得意で、何でも答えるのがあなたの仕事です。関西弁を使って、親しみやすく、カジュアルにユーザーの質問に答えてください。

いい感じに修正されているのがわかる。

なお、本来はこれをイテレーションさせてブラッシュアップしていくと思うので、それにあわせた損失関数を各必要があると思う。以下のnotebookではデータセットを元にした損失関数を用意して、複数回イテレーションで改善していく例が紹介されている。

https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Tutorial-Prompt-Optimization.ipynb

kun432kun432

その他にもいくつかnotebookが紹介されている。

https://github.com/zou-group/textgrad?tab=readme-ov-file#tutorials

  1. Introduction to TextGrad Primitives(初級)
  2. Solution Optimization(初級)
  3. Optimizing a Code Snippet and Define a New Loss(初級)
  4. Prompt Optimization(中級)
    • 一つ上で紹介した、データセットを元にシステムプロンプトを最適化する例が紹介されている
  5. MultiModal Optimization(初級)
    • マルチモーダルLLMを使ったimage-to-textの最適化の例が紹介されている

このあたりは時間があれば試してみたいと思う。

kun432kun432

あとはREADMEにも記載されているが、新しいモデルエンジンの定義としてLiteLLMが試験的に取り入れられている様子、つまり、LiteLLMがサポートしていればどんなLLMを使うこともできる、ということ。

https://github.com/zou-group/textgrad?tab=readme-ov-file#updates

まだ試験的ステータスのようだが、将来的にはこちらに移行するらしいというらしい。

そしてもう一つ、色々試していると気づくのだが、TextGradにはキャッシュ機能があるようで、モデルに入力を与えても結果が変わらない場合がある。現状はこのキャッシュを有効化・無効化する、ということはどうやらできないようで、新しいモデルエンジンではこれらも簡単にできるようになるらしい。

kun432kun432

まとめ

TextGradを知ったのは、実は以下のプロジェクトを試している中で、使用されていたため。

https://github.com/artnoage/Podcast

PDFファイルからポッドキャストを生成するというプロジェクト。過去に試した"PDF2Audio"や、そのベースとなっている"pdf-to-podcast"と同じようなもの。

https://zenn.dev/kun432/scraps/9a2bbf3119d3da

https://github.com/knowsuchagency/pdf-to-podcast

なのだが、この"Podcast"の特徴として、生成したポッドキャスト音声に対してフィードバックを与えて更新することができる機能がある。ここでのフィードバックからのプロンプト最適化にTextGradが使用されているらしい。こちらはこちらで興味があるので、試してみたいと思っている。

で、TextGradの話に戻る。あくまでも個人的な印象になるが、

  • DSPyに比べると、ユーティリティ感があるというか、プロンプト最適化だけではなく、生成された回答やコードの最適化などの用途にも使えるのは良い。
  • PyTorchを理解していれば、多分初期学習コストがかなり少なく始めれるのだろうと思う。

というところになるかな。

残念ながら自分はPyTorchの知見が全くないせいもあって、TextGradのコンセプトを理解するのに時間がかかったし、おそらく今も正確に評価はで来てないように思う。なので、PyTorchの経験がある方にぜひ試していただいてその評価を聞きたいところ、、、

ただ自分でも、一応プロンプト改善みたいなサンプルはサラッと書けたし、なんとなく雰囲気は掴めたので、もう少し色々試してみたいと思ってる。

で、READMEにも「インスパイアされた」と記載がある通り、やはり"DSPy"や"ProTeGi(Gradient Descent)"あたりが比較対象になるかな。以下にDSPyとの比較記事を見つけたので、こちらも参考までに。

https://medium.com/@jelkhoury880/textgrad-vs-dspy-revolutionizing-ai-system-optimization-through-automatic-text-based-58f8ee776447

この手の最適化は、評価等とも関連して、現状でも引き続き難しい課題だと感じていた、やはり自動化する仕組みを作ってやるのが1番望ましいと思う。TextGradもそうだけど、改めて"DSPy"についても再確認してこの辺りもう少し理解したい、

そしてPyTorchにも入門せねば・・・

kun432kun432

なお、余談。

Quick Startで紹介されていた以下のプロンプト

25枚のシャツを天日干しで乾かすのに1時間かかるとしたら、
30枚のシャツを天日干しで乾かすにはどのくらい時間がかかりますか?
ステップバイステップで考えて

これ元はRedditで紹介されていたものらしい
https://www.reddit.com/r/OpenAI/comments/18q479x/reasoning_logic_questions_gpt4_cant_correctly/

で、少し試してみたところ、結構散々な結果だった。

  • ChatGPT
    • gpt-4o-mini: ◯
    • gpt-4o: ✘
    • o1-mini: ✘
    • o1-preview: ✘
  • OpenAI API
    • gpt-4o: ◯
    • gpt-4o-mini: ◯
  • Claude.ai
    • claude-3.5-sonnet: ✘
    • claude-3-ops: ✘
    • claude-3-haiku: ✘

ただAPIだと全然間違えなかったり(なのでサンプルコードはgpt-3.5-turboに書き換えた)、日本語だと間違えないけど英語だと間違える、みたいなところもあって、正直要因がわからない感じではある。

まあ間違える場合は前提条件、というか背景情報が足りないってのが全てではあるんだけど。

このスクラップは1ヶ月前にクローズされました