「DSPy 3.0」を改めて試す ⑥Learning DSPy: Optimization
以下の続き
個人的にはDSPyの最もキモな部分だと思っている「最適化」について。
DSPyにおける最適化の概要
Dia によるまとめ。
DSPyの最適化は、少量のデータでもプロンプトや重みを賢く調整する手順だよ。
DSPyの「最適化」って何?
ウチの感覚だと、これは「レシピの味見と微調整」だもん。最初にプログラム(モジュールやシグネチャ)と評価のものさしを用意したら、プロンプトや重みパラメータを実際のデータで試して、よりハマるようにチューニングしていく感じ。少数事例からでも始められるのがウケるし、実務でも回しやすいんだよね。
使うデータの種類
- 訓練データ(
trainset
): モデルが学ぶ用。- 検証データ(
valset
): 調整の効果を安定に見極める用(過学習チェック)。- テストデータ(
testset
): 最終成績を出す用。ここは最後まで触らないで置いとく、だし。例数は、最初は「30例くらい」でも全然動くけど、目標は最低300例がオススメ。アルゴリズムによっては
trainset
だけでOKなやつもあれば、trainset
とvalset
両方必要なやつもあるでしょ。分割比率のキモ(プロンプト最適化)
深層ニューラルのノリとは逆で、DSPyのプロンプト最適化だと訓練20% / 検証80%くらいが推しだよ。理由は、プロンプト系って小さい訓練データで過学習しがちだから、検証側を厚くして指標の安定性を優先するのがマジで効く。
- 例: 300例なら、訓練60 / 検証240みたいな配分。訓練が少なくて不安?でも検証を厚くして「本当に効いてる?」を見極めるのがポイントだし。
例外:
dspy.GEPA
の考え方
dspy.GEPA
はより「機械学習ガチ勢」っぽい手法だし、ここは普通に訓練データを最大化して、検証は下流タスク(テスト分布)をちゃんと反映できる最小限にするのが推奨。要は「しっかり学ばせつつ、評価は代表性重視」ってノリね。GEPAに興味あるなら、公式の解説も見とくのテンション上がる。Reflective Prompt Evolution with GEPA
https://dspy.ai/tutorials/gepa_ai_program/反復が全て: 戻って見直すのが正義
最初の最適化を数回回すと、「超いい感じ!」か「まだここイケてなくない?」のどっちかになるでしょ。そこでまたプログラム設計に戻って見直すのがDSPyの流儀だし。
- タスク定義はズレてない?
- データ、もっと集めるべき?入手できる外部データある?
- 評価指標は妥当?更新したほうがリアル?
- より高度な最適化やDSPy Assertions使うべき?
- プログラム構造に手順を足す?複雑化させる価値ある?
- 複数の最適化アルゴリズムを順番に使って仕上げる?
この 反復(Iteration) がマジで勝ち筋。DSPyは、データ・プログラム・指標・最適化を少しずつ磨くための道具が揃ってるだもん。
具体イメージ(メタファー付き)
- 料理で言うと、まずレシピ(プログラム)決めて、味見(評価)する。
- 塩やスパイス(プロンプト/重み)をちょい足しして、家族の好み(ビジネス要件)に合わせる。
- 少人数の試食会(小さめtrain)で調整して、大規模な試食会(厚いval)で本当にウケるか確かめる。
- GEPAなら、ガッツリ仕込み(trainを増やす)して、テスト客層に近い小さめ試食でOK出す、て感じ。
実務フローの最短ルート
- 評価基準とプログラムを先に固定(何を「成功」と呼ぶか決める)。
- 開発セットから拡張して、train/val/testを作る(最低30→目標300)。
- プロンプト最適化なら「20/80」で分けて安定性担保。GEPAならtrainを盛ってvalは代表性重視。
- 数回回して、過学習・指標のブレ・本番適合をチェック。
- 設計に戻って再調整(手順追加・指標更新・アルゴリズム変更・アサーション導入など)。
RAGやエージェントなんかだとビジネスルール適合が大事だし、まず「評価指標(例: 一貫性や安全性、使用ポリシー遵守)」を強めに定義してから、20/80でプロンプト最適化→GEPAで詰める流れ、いい線いくと思うだし。
DSPyオプティマイザ(旧称: Teleprompter)
DSPyオプティマイザは、プログラムの「プロンプト」「few-shot例」「LLMの重み」を自動で最適化して、評価指標を最大化、つまり入力に対して期待する出力を得る精度を高めるための仕組み、といえる。
オプティマイザは通常、以下の3つの要素を使用する。
- DSPyプログラム: 単一モジュールでも複数モジュールでもOK)。
- 評価指標: 出力にスコアを付ける関数。高いほど評価が高い。
- 学習用入力データ: 少数でもラベルなしでも実施できる(とはいえ十分なデータ量があるようが良い)
なお、オプティマイザは以前のDSPyでは 「Teleprompter」と呼ばれていたが、名前が変更された様子。
DSPyオプティマイザは何を?どう?調整するのか
オプティマイザは基本的に以下の3つのやり方で最適化を行う。
-
few-shot例の自動生成
- 高品質な回答例を作成してプロンプトに差し込む
- 例:
dspy.BootstrapRS
-
自然言語の指示最適化
- プロンプトの「指示文」をより的確に。
- 例:
dspy.MIPROv2
、dspy.GEPA
-
重みのファインチューニング
- 各モジュール用にデータセットを作ってLLM自体を学習。
- 例:
dspy.BootstrapFinetune
例として dspy.MIPROv2
がどのような最適化を行うのかが記載されている
-
ブートストラップ段階
- 現状のプログラムにいろいろな入力を行い、各モジュールの入出力トレースを収集。
- 評価メトリクスで高スコアなものだけを残す。
-
接地型提案段階(grounding proposal stage)
- プログラムのコード、データ、トレースを分析
- 各プロンプト向けに複数の「指示文」候補を作成。
-
離散探索段階(discrete search stage)
- ミニバッチで候補プログラムを評価
- スコアをもとに代理モデル更新
- 良い指示・例の組み合わせを学習して精度を詰めていく
また、さらにオプティマイザを組み合わせることもできる。例えば以下のような使い方。
-
dspy.MIPROv2
で作ったプログラムをもう一回dspy.MIPROv2
に入れる -
または
dspy.BootstrapFinetune`へ渡して、LLMのファインチューニングでさらに精度を上げる -
dspy.BetterTogether
は上記をまとめたもの(参考) -
dspy.Ensemble
: 上位5候補をまとめたアンサンブル手法で推論精度を上げる
現在利用可能なDSPyオプティマイザの種類
ざっくり以下のような感じ。詳細は各オプティマイザを参照。
カテゴリ | オプティマイザ | 何をする? | ユースケース |
---|---|---|---|
自動Few-Shot学習 | LabeledFewShot | ラベル付きデータからそのまま少数ショット例を構築。 | 手元に正解付きの小さいデータがある。 |
BootstrapFewShot | teacherで例を生成+評価で良い例だけ選抜。 | ラベル少なめでも例を増やして質を担保したい。 | |
BootstrapFewShotWithRandomSearch | BootstrapFewShotを複数回+ランダム探索でベスト選抜。 | データ50例以上でより強い構成を探したい。 | |
KNNFewShot | k近傍で入力に近いデモを選ぶ。 | 入力ごとに似た事例を当てたい。 | |
指示最適化 | COPRO | 山登り法で各ステップの指示を反復改善。 | 手早く指示文だけブラッシュアップ。 |
MIPROv2 | 指示+少数ショット両方をベイズ最適化で探索。 | 長めの探索でガッツリ精度を上げたい。 | |
SIMBA | 自己反省ルールを生成+成功例をデモ追加しながらミニバッチで改善。 | 実行ログから学ぶ自己反省型の改善が効く場面。出力のばらつきが大きい課題で強い。 | |
GEPA | 成功/失敗トレースからギャップを埋める指示をLMが提案。 | 実運用ログを活用して改善を急ぎたい。 | |
ファインチューニング | BootstrapFinetune | プロンプト手続を重み更新に変換してタスク特化モデル化。 | 7B以上を使える&効率重視で小型に落としたい。 |
変換 | Ensemble | 複数プログラムを単一にまとめる(全使用orサブセット)。 | アンサンブルで推論精度をさらに押し上げたい。 |
冒頭のところで、オプティマイザの以前の名称は teleprompter と書いてあったが、このあたりの名残りはまだ残っているようで、例えばドキュメントには以下のようにすれば全部のオプティマイザがインポートされるとある。
from dspy.teleprompt import *
ただし、実際に使う場合は上記を指定する必要はなく、import dspy
で全部アクセスできると思う。
オプティマイザの選択
タスク合わせて最適なオプティマイザをどう選択するか?は基本的には試行錯誤するしかないが、基本的な選択時のガイドラインが記載されている
- サンプルが少ない(10例程度)なら、まず
BootstrapFewShot
。 - データ豊富(50例+)なら、
BootstrapFewShotWithRandomSearch
。 - 指示だけを最適化したいなら、
MIPROv2
を0ショット設定で。 - 長い最適化(40試行+)&十分なデータ(200例+)があるなら、
MIPROv2
。 - 効率重視&7B以上の言語モデルが使えるなら、
BootstrapFinetune
で小型タスク特化モデルにファインチューニング。
オプティマイザの使い方
オプティマイザーは基本的に共通のインターフェースとなっている。ただし、キーワード引数(ハイパーパラメータ)はオプティマイザによって若干異なる場合がある様子。実際に使う場合は APIリファレンス を参照すればよい。
基本的な例としてBootstrapFewShotWithRandomSearch
が挙げられている。
from dspy.teleprompt import BootstrapFewShotWithRandomSearch
# オプティマイザを設定: プログラムの各ステップについて、8ショットの例を「ブートストラップ」(自己生成)したい場合の例。
# オプティマイザは開発セットで最も優れた試行を選択する前に、このプロセスを10回繰り返す(初期試行数を含む)。
# オプティマイザのパラメータを設定
config = dict(
max_bootstrapped_demos=4,
max_labeled_demos=4,
num_candidate_programs=10,
num_threads=4
)
# メトリクスと設定を渡して、オプティマイザを初期化
teleprompter = BootstrapFewShotWithRandomSearch(
metric=<メトリックを指定>,
**config
)
# プログラムとデータセットを渡して、オプティマイザでコンパイル(最適化)
optimized_program = teleprompter.compile(
<プログラムを指定>,
trainset=<学習データセットを指定>
)
上にも書いた通り、telepromopter というのは古い名称で、そのうちコードも書き換わるのだろう。Get Startedにもあった最適化の例が3つ、ここにも記載されていて、そちらのほうが全部の処理で書いてあるのでわかりやすいと思う。
オプティマイザの出力の保存と読み込み
最適化済みのプログラムは保存して読み込むことができる。保存は save()
、読み込みは load()
。
optimized_program.save(<保存先のパス>)
loaded_program = <プログラムのクラス>()
loaded_program.load(path=<保存先のパス>)
保存するとテキスト形式のJSONファイルで保存される。このファイルには元のプログラムのすべてのパラメータと処理ステップが含まれている。
これも実際にいろいろ試してもう少し把握しておきたいところ。