Home 論文解説: MIPROv2 — ベイズ最適化による多段LLMプログラムの命令・デモ最適化
投稿
キャンセル

📄 論文解説: MIPROv2 — ベイズ最適化による多段LLMプログラムの命令・デモ最適化

論文概要(Abstract)

MIPROv2(Multi-prompt Instruction PRoposal Optimizer Version 2)は、複数のLM呼び出しモジュールで構成されるDSPyプログラムに対して、各モジュールの命令文(instruction)とfew-shotデモンストレーションを同時にベイズ最適化するアルゴリズムである。著者らは、プログラム構造やデータを考慮した命令提案手法、ミニバッチ評価による代理モデル学習、および提案生成自体を改善するメタ最適化の3つの戦略を導入している。Llama-3-8Bを用いた評価で、7つの多段LMプログラムのうち5つでベースラインを上回り、最大13%の精度向上を達成したと報告されている。EMNLP 2024に採録された研究である。

この記事は Zenn記事: DSPy v3.1 GEPA×Evaluateで構築するプロンプト最適化自動化パイプライン の深掘りです。

情報源

項目内容
arXiv ID2406.11695
タイトルOptimizing Instructions and Demonstrations for Multi-Stage Language Model Programs
著者Krista Opsahl-Ong, Michael J Ryan, Josh Purtell, et al. (Krista・Michael は共同筆頭著者)
発表年2024(EMNLP 2024 採録)
分野cs.CL(自然言語処理)/ cs.AI
コードhttps://dspy.ai (DSPyフレームワークに統合)

背景と動機

近年、単一のLM呼び出しではなく、検索・推論・分類などの複数モジュールをパイプラインとして連結するLMプログラムが注目されている。DSPyはこのパラダイムを支えるフレームワークであるが、各モジュールのプロンプト(命令文とfew-shotデモ)を手動で調整するコストは、モジュール数に応じて指数的に増大する。

既存のプロンプト最適化手法には2つの課題があった。第一に、OPRO(Yang et al., 2024)のようなモジュール単位の手法は、モジュール間の相互作用(クレジット割り当て問題)を無視する。第二に、命令文とfew-shotデモの同時最適化を扱うアルゴリズムが存在しなかった。著者らはこれらの課題を解決するため、ベイズ最適化を用いてプログラム全体を横断的に最適化するMIPROv2を提案している。

主要な貢献

著者らは以下の5つの貢献を報告している。

  • 命令文とfew-shotデモの同時最適化: 各モジュールの命令候補とデモ候補を離散カテゴリカル変数として定義し、TPEベースのベイズ最適化で最適な組み合わせを探索する手法を提案
  • データ・プログラム認識型の命令提案: データセットの要約、プログラムの制御フロー記述、ブートストラップ済みデモ、過去の命令・スコア履歴の4種類のグラウンディング情報を用いた命令生成
  • ミニバッチ評価による効率化: TPEのノイズに対する頑健性を活かし、検証セット全体ではなくランダムサブセットで評価することで計算コストを削減
  • メタ最適化(MIPRO++): 命令提案に使うハイパーパラメータ(温度、グラウンディング要素の選択)自体をベイズ最適化する手続きを導入し、ScoNeタスクで0-shot MIPRO(71.5%)から75.7%への改善を確認
  • 体系的なベンチマーク: 6種類のタスク・7プログラム構成で、命令のみ/デモのみ/同時最適化の効果を定量比較

技術的詳細

問題定式化

LMプログラム \(\Phi\) は、複数のプロンプトテンプレートを持ち、各テンプレートには変数 \(V\) がある。最適化目標は以下のように定式化される。

\[\Phi^* = \arg\max_{V \mapsto S} \frac{1}{|D|} \sum_{(x, x') \in D} \mu(\Phi_{V \mapsto S}(x), x')\]

ここで \(S\) は各変数への文字列割り当て、\(D\) は訓練データ、\(\mu\) はタスク固有の評価指標である。変数 \(V\) は「命令文」と「few-shotデモ集合」の2種類に分解される。

TPE(Tree-structured Parzen Estimator)による探索

MIPROv2はOptunaのTPEサンプラー(multivariate=True)を使用する。TPEは、観測されたトライアルをスコアの良い群と悪い群に分割し、それぞれの分布を推定する。

分位点による分割: \(\gamma\) 個の「良い」トライアル群と残りの「悪い」群に分ける。Optunaのデフォルトでは以下が用いられる。

\[\gamma(n) = \min\left(\lceil 0.1n \rceil,\ 25\right)\]

2つの密度モデル: 良い群のパラメータ分布 \(\ell(x)\) と悪い群の分布 \(g(x)\) をガウス混合モデル(GMM)で推定する。

\[\ell(x) = \frac{1}{|\mathcal{D}_{\text{good}}|+1} \left( w_{\text{prior}} + \sum_{j \in \mathcal{D}_{\text{good}}} \mathcal{N}(x \mid \mu_j, \sigma_j) \right)\]

獲得関数: Expected Improvement(EI)に比例する以下の比率を最大化する候補を選択する。

\[\text{EI}(x) \propto \frac{\ell(x)}{g(x)}\]

この比率が高い領域は「良いトライアルが集中し、悪いトライアルが少ない」パラメータ空間であり、次に評価すべき有望な候補を示す。

探索空間の構成

各Predictorモジュール \(i\) に対して、以下の離散カテゴリカル変数が定義される。

  • 命令変数 {i}_predictor_instruction: 生成された \(N\) 個の命令候補からの選択(インデックス 0 は元の命令)
  • デモ変数 {i}_predictor_demos: ブートストラップされた \(N\) 個のデモセットからの選択

モジュール数が \(M\)、各モジュールの候補数が \(N\) の場合、探索空間のサイズは \(N^{2M}\) となる。

アルゴリズム全体像

flowchart TD
    A[訓練データ D] --> B[Phase 1: Bootstrap]
    B --> B1[ランダムサンプリング]
    B1 --> B2[プログラム実行・正解判定]
    B2 --> B3[成功トレースをデモ候補に蓄積]
    B3 --> C[Phase 2: Propose]
    C --> C1[データセット要約を生成]
    C --> C2[プログラム構造を記述]
    C --> C3[ブートストラップデモを参照]
    C --> C4[ランダムtipを選択]
    C1 --> C5[GroundedProposer: 命令候補N個を生成]
    C2 --> C5
    C3 --> C5
    C4 --> C5
    C5 --> D1[Phase 3: Bayesian Search]
    B3 --> D1
    D1 --> D2[TPEが命令・デモの組み合わせを提案]
    D2 --> D3[ミニバッチで評価]
    D3 --> D4{full_eval_steps?}
    D4 -->|Yes| D5[検証セット全体で評価]
    D4 -->|No| D6[TPEサロゲートモデルを更新]
    D5 --> D6
    D6 --> D7{試行回数 < num_trials?}
    D7 -->|Yes| D2
    D7 -->|No| E[最高スコアの組み合わせを返却]

命令提案の実装(擬似コード)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from typing import List, Dict, Any, Optional
import dspy

class GroundedProposer:
    """データ・プログラム認識型の命令提案モジュール。

    4種類のグラウンディング情報を組み合わせ、
    多様な命令候補を生成する。
    """

    TIPS: List[str] = [
        "be creative",
        "be concise",
        "use step-by-step reasoning",
        "focus on accuracy",
        "be specific about format",
        "think about edge cases",
    ]

    def __init__(
        self,
        prompt_model: dspy.LM,
        program_aware: bool = True,
        data_aware: bool = True,
        tip_aware: bool = True,
        fewshot_aware: bool = True,
    ) -> None:
        self.prompt_model = prompt_model
        self.program_aware = program_aware
        self.data_aware = data_aware
        self.tip_aware = tip_aware
        self.fewshot_aware = fewshot_aware

    def propose_instructions(
        self,
        module: dspy.Module,
        trainset: List[dspy.Example],
        demos: Optional[List[dspy.Example]] = None,
        num_candidates: int = 10,
    ) -> List[str]:
        """指定モジュールに対する命令候補を生成する。

        Args:
            module: 命令を最適化する対象のDSPyモジュール
            trainset: 訓練データセット
            demos: ブートストラップ済みデモンストレーション
            num_candidates: 生成する命令候補の数

        Returns:
            命令文字列のリスト(先頭要素は元の命令)
        """
        context: Dict[str, Any] = {}

        if self.data_aware:
            context["dataset_summary"] = self._summarize_dataset(trainset)
        if self.program_aware:
            context["program_description"] = self._describe_program(module)
        if self.fewshot_aware and demos:
            context["demo_examples"] = demos[:3]

        instructions: List[str] = [module.signature.instructions]  # slot 0 = 元の命令

        for _ in range(num_candidates - 1):
            if self.tip_aware:
                import random
                context["tip"] = random.choice(self.TIPS)

            candidate = self._generate_instruction(context)
            instructions.append(candidate)

        return instructions

    def _summarize_dataset(self, trainset: List[dspy.Example]) -> str:
        """DatasetDescriptorでデータセットの統計的要約を生成する。"""
        ...

    def _describe_program(self, module: dspy.Module) -> str:
        """DescribeModule/DescribeProgramでモジュール構造を記述する。"""
        ...

    def _generate_instruction(self, context: Dict[str, Any]) -> str:
        """コンテキストに基づきGenerateSingleModuleInstructionで命令を生成する。"""
        ...

ミニバッチ評価の仕組み

通常のベイズ最適化では各トライアルを全検証セットで評価するが、これはLM呼び出しコストが高い場合に非現実的である。MIPROv2はTPEがノイズに頑健であることを活かし、以下の2段階評価を採用する。

  1. ミニバッチ評価: 各トライアルをランダムな35件のサブセットで評価
  2. 定期的な全体評価: minibatch_full_eval_steps(デフォルト5)トライアルごとに、最高スコアの候補を全検証セットで再評価

これにより、論文のベンチマークではトライアルあたりの評価コストを約85-93%削減しつつ、最終的な最適化品質を維持している。

実装のポイント

ハイパーパラメータガイド

パラメータ推奨値説明
num_candidates7-10モジュールあたりの命令・デモ候補数
max_bootstrapped_demos3-4ブートストラップデモの最大数(品質は数より重要)
max_labeled_demos4-16ラベル付きデモの最大数(タスク複雑度に依存)
init_temperature0.5-1.0命令生成の多様性を制御
minibatch_size20-50安定した評価のためのバッチサイズ
minibatch_full_eval_steps5-20検証ノイズが高い場合は大きく設定
seed9(固定値)再現性確保のための乱数シード

autoモードの比較

モードnum_candidatesnum_trials検証セット上限用途
light3-510-15100件プロトタイピング
medium6-820-30300件標準的な最適化
heavy10+40-50+1000件本番システム向け

num_trialsの決め方

著者らは以下の目安を提示している。

\[\text{num\_trials} \geq 0.75 \times M \times K\]

ここで \(M\) はモジュール数、\(K\) は変数種別数(命令+デモなら2、0-shotなら1)である。例えば2モジュール・命令+デモの場合、最低 \(0.75 \times 2 \times 2 = 3\) トライアルだが、実用上は15-30トライアルが推奨される。

実装例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import dspy
from dspy.teleprompt import MIPROv2

def setup_miprov2_optimization(
    program: dspy.Module,
    trainset: list[dspy.Example],
    metric: callable,
    mode: str = "medium",
) -> dspy.Module:
    """MIPROv2による最適化のセットアップと実行。

    Args:
        program: 最適化対象のDSPyプログラム
        trainset: 訓練データセット
        metric: 評価関数(Example -> float)
        mode: 最適化モード(light/medium/heavy)

    Returns:
        最適化済みのDSPyプログラム
    """
    optimizer = MIPROv2(
        metric=metric,
        auto=mode,
        num_threads=4,
        max_bootstrapped_demos=4,
        max_labeled_demos=4,
        verbose=True,
        track_stats=True,
    )

    optimized = optimizer.compile(
        program,
        trainset=trainset,
        minibatch=True,
        minibatch_size=35,
        minibatch_full_eval_steps=5,
        program_aware_proposer=True,
        data_aware_proposer=True,
        tip_aware_proposer=True,
        fewshot_aware_proposer=True,
    )

    return optimized

実験結果

ベンチマーク概要

著者らは6種類のタスク・7つのプログラム構成で評価を行っている(論文Table 2)。

タスク種別モジュール数評価指標Train / Dev / Test
HotPotQAMulti-Hop QA2Exact Match500 / 500 / 2000
HotPotQA ConditionalMulti-Hop QA2Custom500 / 200 / 200
Iris分類1Accuracy75 / - / 75
Heart Disease分類2Accuracy120 / - / 183
ScoNe自然言語推論1Exact Match500 / 500 / 1200
HoVer検索4Retrieval@21500 / 500 / 1520

主要結果(テスト精度、Llama-3-8B使用)

以下は論文Table 2から抜粋した代表的な結果である。

手法ScoNeHotPotQAHeart DiseaseHoVer
0-Shot(命令のみ)56.2%36.1%-25.3%
Bootstrap Random(デモのみ)75.4%45.8%79.2%-
MIPRO(同時最適化)79.4%46.4%-39.0%

著者らは以下の知見を報告している。

  1. デモは命令より効果が大きい: 大半のタスクで、命令のみの最適化よりデモ追加の効果が顕著
  2. 同時最適化は6タスク中5タスクで最良: ScoNe(+4.0pp vs デモのみ)やHoVer(+13.7pp vs 命令のみ)で顕著な改善
  3. 条件的ルールには命令が有効: ScoNeのように「否定が含まれる場合のみ推論を反転させる」といったルールはデモだけでは学習困難
  4. グラウンディングの効果はタスク依存: HotPotQA/HoVerでは有効だがScoNeでは逆効果。MIPRO++はこの選択を自動化
  5. MIPRO++の効果: 0-shot MIPROの71.5%からMIPRO++は75.7%に改善(ScoNe)

統計的有意性

著者らはWilcoxon符号付き順位検定(\(p < 0.05\))を用いて、改善の統計的有意性を検証している。

使用モデルと計算コスト

役割モデル
タスク実行Llama-3-8B(temperature 0.7)
命令提案GPT-3.5(デフォルト)/ GPT-4(ScoNe, HoVer)
デモブートストラップLlama-3-8B / GPT-4(複雑タスク)

トライアル数は、HotPotQA・ScoNeで50回の全体評価(約300回のミニバッチ評価に相当)、その他タスクで20-30回と報告されている。

実運用への応用

Zenn記事との接続

Zenn記事で解説されているDSPy v3.1のGEPA(Generate, Evaluate, Propose, Accept)パイプラインにおいて、MIPROv2は「Propose」と「Accept」のステップを自動化する役割を担う。手動でプロンプトを調整する代わりに、MIPROv2がデータ駆動で最適な命令・デモの組み合わせを発見する。

本番適用時の留意点

  1. 計算コストの見積もり: num_trials × minibatch_size 回のLM呼び出しが最低限必要。LLM APIのレート制限とコストを事前に確認する
  2. モデル分離戦略: prompt_modelに高性能モデル(GPT-4等)、task_modelに低コストモデル(Llama-3等)を割り当てることで、命令品質と実行コストを両立できる
  3. 検証セットの品質: ミニバッチ評価の精度はデータの代表性に依存する。偏ったサンプリングは局所最適に陥るリスクがある
  4. 再最適化のタイミング: モデルの更新やデータ分布の変化時に再最適化が必要。CIパイプラインに組み込むことを推奨

Production Deployment Guide

AWS実装パターン

MIPROv2を本番環境に導入する場合、以下の3つの構成が考えられる。

構成想定ユースケース月額概算(USD)
Small(サーバーレス)日次バッチ最適化、1-2パイプライン$150-400
Medium(ECS + ElastiCache)複数パイプライン、リアルタイムフィードバック$800-1,500
Large(EKS + SageMaker)大規模マルチテナント、継続的最適化$3,000-8,000

Small構成の概要

1
2
3
4
5
6
7
Lambda (最適化トリガー)
  → Step Functions (ワークフロー管理)
    → Lambda (MIPROv2実行、メモリ10GB、タイムアウト15分)
      → Bedrock / 外部LLM API (タスク実行・命令提案)
    → S3 (最適化結果・候補保存)
    → DynamoDB (トライアル履歴・メトリクス)
  → SNS (完了通知)

Medium構成の概要

1
2
3
4
5
API Gateway → ECS Fargate (MIPROv2ワーカー)
  → ElastiCache Redis (候補キャッシュ、トライアル状態)
  → RDS PostgreSQL (メトリクス履歴、A/Bテスト結果)
  → Bedrock / SageMaker Endpoint (LM推論)
  → S3 (最適化アーティファクト)

Large構成の概要

1
2
3
4
5
6
ALB → EKS (MIPROv2オーケストレーター + ワーカー)
  → SageMaker (モデルホスティング、バッチ推論)
  → Aurora PostgreSQL (マルチテナントメトリクス)
  → ElastiCache Redis Cluster (分散キャッシュ)
  → Kinesis (リアルタイムメトリクスストリーム)
  → S3 (長期アーティファクト保存)

Terraformインフラコード(Small構成)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

# --- S3: 最適化結果の保存 ---
resource "aws_s3_bucket" "miprov2_artifacts" {
  bucket = "miprov2-optimization-artifacts"
}

resource "aws_s3_bucket_versioning" "miprov2_artifacts" {
  bucket = aws_s3_bucket.miprov2_artifacts.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "miprov2_artifacts" {
  bucket = aws_s3_bucket.miprov2_artifacts.id

  rule {
    id     = "expire-old-artifacts"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }

    expiration {
      days = 180
    }
  }
}

# --- DynamoDB: トライアル履歴 ---
resource "aws_dynamodb_table" "trial_history" {
  name         = "miprov2-trial-history"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "pipeline_id"
  range_key    = "trial_id"

  attribute {
    name = "pipeline_id"
    type = "S"
  }

  attribute {
    name = "trial_id"
    type = "S"
  }

  ttl {
    attribute_name = "expires_at"
    enabled        = true
  }

  tags = {
    Project = "miprov2-optimizer"
  }
}

# --- IAM: Lambda実行ロール ---
resource "aws_iam_role" "miprov2_lambda" {
  name = "miprov2-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "miprov2_lambda" {
  name = "miprov2-lambda-policy"
  role = aws_iam_role.miprov2_lambda.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:ListBucket"
        ]
        Resource = [
          aws_s3_bucket.miprov2_artifacts.arn,
          "${aws_s3_bucket.miprov2_artifacts.arn}/*"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "dynamodb:PutItem",
          "dynamodb:GetItem",
          "dynamodb:Query",
          "dynamodb:UpdateItem"
        ]
        Resource = aws_dynamodb_table.trial_history.arn
      },
      {
        Effect = "Allow"
        Action = [
          "bedrock:InvokeModel",
          "bedrock:InvokeModelWithResponseStream"
        ]
        Resource = "arn:aws:bedrock:ap-northeast-1::foundation-model/*"
      },
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:*:*:*"
      }
    ]
  })
}

# --- Lambda: MIPROv2最適化ワーカー ---
resource "aws_lambda_function" "miprov2_worker" {
  function_name = "miprov2-optimization-worker"
  role          = aws_iam_role.miprov2_lambda.arn
  handler       = "handler.optimize"
  runtime       = "python3.12"
  timeout       = 900
  memory_size   = 10240

  filename         = "lambda_package.zip"
  source_code_hash = filebase64sha256("lambda_package.zip")

  environment {
    variables = {
      ARTIFACTS_BUCKET    = aws_s3_bucket.miprov2_artifacts.id
      TRIAL_HISTORY_TABLE = aws_dynamodb_table.trial_history.name
      MIPROV2_AUTO_MODE   = "medium"
      LOG_LEVEL           = "INFO"
    }
  }

  tags = {
    Project = "miprov2-optimizer"
  }
}

# --- Step Functions: ワークフロー管理 ---
resource "aws_sfn_state_machine" "miprov2_pipeline" {
  name     = "miprov2-optimization-pipeline"
  role_arn = aws_iam_role.step_functions.arn

  definition = jsonencode({
    Comment = "MIPROv2 Optimization Pipeline"
    StartAt = "ValidateInput"
    States = {
      ValidateInput = {
        Type     = "Task"
        Resource = aws_lambda_function.miprov2_worker.arn
        Parameters = {
          "action"    = "validate"
          "input.$"   = "$"
        }
        Next = "RunOptimization"
      }
      RunOptimization = {
        Type     = "Task"
        Resource = aws_lambda_function.miprov2_worker.arn
        Parameters = {
          "action"      = "optimize"
          "config.$"    = "$.config"
          "trainset.$"  = "$.trainset_s3_key"
        }
        TimeoutSeconds = 900
        Retry = [
          {
            ErrorEquals     = ["States.TaskFailed"]
            IntervalSeconds = 60
            MaxAttempts     = 2
            BackoffRate     = 2.0
          }
        ]
        Next = "SaveResults"
      }
      SaveResults = {
        Type     = "Task"
        Resource = aws_lambda_function.miprov2_worker.arn
        Parameters = {
          "action"     = "save"
          "results.$"  = "$.optimization_results"
        }
        Next = "NotifyComplete"
      }
      NotifyComplete = {
        Type     = "Task"
        Resource = "arn:aws:states:::sns:publish"
        Parameters = {
          "TopicArn"  = aws_sns_topic.miprov2_notifications.arn
          "Message.$" = "States.Format('MIPROv2 optimization complete: {}', $.optimization_results.best_score)"
        }
        End = true
      }
    }
  })
}

resource "aws_iam_role" "step_functions" {
  name = "miprov2-stepfunctions-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "states.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "step_functions" {
  name = "miprov2-stepfunctions-policy"
  role = aws_iam_role.step_functions.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = "lambda:InvokeFunction"
        Resource = aws_lambda_function.miprov2_worker.arn
      },
      {
        Effect   = "Allow"
        Action   = "sns:Publish"
        Resource = aws_sns_topic.miprov2_notifications.arn
      }
    ]
  })
}

# --- SNS: 完了通知 ---
resource "aws_sns_topic" "miprov2_notifications" {
  name = "miprov2-optimization-notifications"
}

# --- EventBridge: 日次トリガー ---
resource "aws_cloudwatch_event_rule" "daily_optimization" {
  name                = "miprov2-daily-trigger"
  description         = "Trigger MIPROv2 optimization daily at 03:00 JST"
  schedule_expression = "cron(0 18 * * ? *)"
}

resource "aws_cloudwatch_event_target" "step_functions" {
  rule     = aws_cloudwatch_event_rule.daily_optimization.name
  arn      = aws_sfn_state_machine.miprov2_pipeline.arn
  role_arn = aws_iam_role.eventbridge.arn

  input = jsonencode({
    config = {
      auto_mode    = "medium"
      num_threads  = 4
      minibatch    = true
    }
    trainset_s3_key = "datasets/current/trainset.json"
  })
}

resource "aws_iam_role" "eventbridge" {
  name = "miprov2-eventbridge-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "events.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "eventbridge" {
  name = "miprov2-eventbridge-policy"
  role = aws_iam_role.eventbridge.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = "states:StartExecution"
        Resource = aws_sfn_state_machine.miprov2_pipeline.arn
      }
    ]
  })
}

運用・監視設定

CloudWatch ダッシュボード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
resource "aws_cloudwatch_dashboard" "miprov2" {
  dashboard_name = "MIPROv2-Optimization"

  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6
        properties = {
          metrics = [
            ["AWS/Lambda", "Duration", "FunctionName",
             aws_lambda_function.miprov2_worker.function_name,
             { stat = "Average", period = 86400 }],
            ["AWS/Lambda", "Errors", "FunctionName",
             aws_lambda_function.miprov2_worker.function_name,
             { stat = "Sum", period = 86400 }]
          ]
          title  = "Lambda Execution Metrics (Daily)"
          region = "ap-northeast-1"
        }
      },
      {
        type   = "metric"
        x      = 12
        y      = 0
        width  = 12
        height = 6
        properties = {
          metrics = [
            ["AWS/DynamoDB", "ConsumedReadCapacityUnits",
             "TableName", aws_dynamodb_table.trial_history.name,
             { stat = "Sum", period = 3600 }],
            ["AWS/DynamoDB", "ConsumedWriteCapacityUnits",
             "TableName", aws_dynamodb_table.trial_history.name,
             { stat = "Sum", period = 3600 }]
          ]
          title  = "DynamoDB Throughput (Hourly)"
          region = "ap-northeast-1"
        }
      },
      {
        type   = "metric"
        x      = 0
        y      = 6
        width  = 24
        height = 6
        properties = {
          metrics = [
            ["AWS/States", "ExecutionsSucceeded",
             "StateMachineArn", aws_sfn_state_machine.miprov2_pipeline.arn,
             { stat = "Sum", period = 86400 }],
            ["AWS/States", "ExecutionsFailed",
             "StateMachineArn", aws_sfn_state_machine.miprov2_pipeline.arn,
             { stat = "Sum", period = 86400 }],
            ["AWS/States", "ExecutionTime",
             "StateMachineArn", aws_sfn_state_machine.miprov2_pipeline.arn,
             { stat = "Average", period = 86400 }]
          ]
          title  = "Step Functions Pipeline (Daily)"
          region = "ap-northeast-1"
        }
      }
    ]
  })
}

CloudWatch Alarms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
resource "aws_cloudwatch_metric_alarm" "lambda_errors" {
  alarm_name          = "miprov2-lambda-errors"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "Errors"
  namespace           = "AWS/Lambda"
  period              = 86400
  statistic           = "Sum"
  threshold           = 3
  alarm_description   = "MIPROv2 Lambda errors exceed threshold"

  dimensions = {
    FunctionName = aws_lambda_function.miprov2_worker.function_name
  }

  alarm_actions = [aws_sns_topic.miprov2_notifications.arn]
}

resource "aws_cloudwatch_metric_alarm" "optimization_duration" {
  alarm_name          = "miprov2-optimization-slow"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "ExecutionTime"
  namespace           = "AWS/States"
  period              = 86400
  statistic           = "Average"
  threshold           = 600000
  alarm_description   = "MIPROv2 optimization exceeds 10 minutes"

  dimensions = {
    StateMachineArn = aws_sfn_state_machine.miprov2_pipeline.arn
  }

  alarm_actions = [aws_sns_topic.miprov2_notifications.arn]
}

X-Ray トレーシング

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
resource "aws_lambda_function" "miprov2_worker_xray" {
  # ... (既存のLambda設定に追加)
  tracing_config {
    mode = "Active"
  }
}

resource "aws_xray_sampling_rule" "miprov2" {
  rule_name      = "miprov2-sampling"
  priority       = 1000
  version        = 1
  reservoir_size = 1
  fixed_rate     = 0.1
  url_path       = "*"
  host           = "*"
  http_method    = "*"
  service_type   = "*"
  service_name   = "miprov2-*"
  resource_arn   = "*"
}

Cost Explorer タグベースのコスト追跡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
resource "aws_ce_cost_category" "miprov2" {
  name = "MIPROv2-Costs"

  rule {
    value = "MIPROv2-Optimization"
    rule {
      tags {
        key    = "Project"
        values = ["miprov2-optimizer"]
      }
    }
  }

  rule {
    value = "MIPROv2-LLM-API"
    rule {
      tags {
        key    = "CostCenter"
        values = ["llm-api-calls"]
      }
    }
  }
}

resource "aws_budgets_budget" "miprov2_monthly" {
  name         = "miprov2-monthly-budget"
  budget_type  = "COST"
  limit_amount = "500"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  cost_filter {
    name   = "TagKeyValue"
    values = ["user:Project$miprov2-optimizer"]
  }

  notification {
    comparison_operator       = "GREATER_THAN"
    threshold                 = 80
    threshold_type            = "PERCENTAGE"
    notification_type         = "ACTUAL"
    subscriber_email_addresses = ["admin@example.com"]
  }

  notification {
    comparison_operator       = "GREATER_THAN"
    threshold                 = 100
    threshold_type            = "PERCENTAGE"
    notification_type         = "FORECASTED"
    subscriber_email_addresses = ["admin@example.com"]
  }
}

コスト最適化チェックリスト

LLM API コスト

  • prompt_modeltask_model を分離し、命令提案には高性能モデル、タスク実行には低コストモデルを使用
  • minibatch=True を有効化し、全検証セット評価の頻度を削減
  • minibatch_size を必要最小限(20-35)に設定
  • num_candidates を過剰に設定していないか確認(7-10が目安)
  • num_trials0.75 * M * K の下限から大きく乖離していないか確認
  • 不要なグラウンディング要素(program_aware等)を無効化してトークン消費を削減
  • Bedrock Provisioned Throughputの利用を検討(安定した大量リクエスト時)

コンピュート コスト

  • Lambda関数のメモリサイズを実測に基づいて調整(Power Tuning活用)
  • Lambda関数のタイムアウトを実行時間の1.5倍に設定
  • Step Functionsの Express Workflow を検討(5分以内の実行ケース)
  • 不要な再試行回数を削減(MaxAttempts の見直し)
  • Graviton(ARM64)ランタイムの採用でLambdaコストを20%削減

ストレージ コスト

  • S3ライフサイクルポリシーで古いアーティファクトをStandard-IAに移行
  • DynamoDB TTLで期限切れトライアル履歴を自動削除
  • S3 Intelligent-Tieringの活用を検討
  • 不要なバージョニング対象を限定

ネットワーク コスト

  • LLM APIとLambdaを同一リージョンに配置
  • VPCエンドポイントでS3/DynamoDBへのアクセスをプライベート化
  • NAT Gatewayの不要利用を排除

運用 コスト

  • CloudWatch Logsの保持期間を90日以下に設定
  • X-Rayサンプリングレートを本番は10%以下に抑制
  • CloudWatchメトリクスのカスタム名前空間を最小限に
  • 未使用のアラームやダッシュボードウィジェットを削除
  • AWS Budgets アラートの閾値を80%と100%(予測)に設定

関連研究

OPRO(Yang et al., 2024)

LMに過去のプロンプト-スコアペアを入力し、改善提案を生成するメタプロンプティング手法。MIPROv2との違いは、OPROがモジュール単独の最適化に留まるのに対し、MIPROv2はプログラム全体の組み合わせ最適化を行う点である。

DSPy BootstrapFewShotWithRandomSearch

デモンストレーションのランダム探索による最適化。MIPROv2はこの手法のブートストラップ戦略を Phase 1 で継承しつつ、TPEによる探索でランダムサーチを置き換えている。論文のベンチマークでは、MIPROv2が多くのタスクでこのベースラインを上回ったと報告されている。

APE(Automatic Prompt Engineer, Zhou et al., 2023)

LMを用いてプロンプト候補を生成し、評価ベースで選択するフレームワーク。MIPROv2はAPEの命令生成アイデアを拡張し、データ・プログラム認識型のグラウンディングとベイズ最適化による効率的な探索を追加している。

Optuna / TPE(Watanabe, 2023)

MIPROv2が採用するベイズ最適化バックエンドの基盤研究。TPEの多変量版(multivariate=True)を用いることで、異なるモジュール間のパラメータ相関をモデル化できる点が、プロンプト最適化において重要であると著者らは述べている。

まとめと今後の展望

MIPROv2は、多段LMプログラムにおける命令文とfew-shotデモの同時最適化を、TPEベースのベイズ最適化で実現したアルゴリズムである。Llama-3-8Bでの評価で最大13%の精度改善を達成し、ミニバッチ評価やメタ最適化(MIPRO++)といった実用的な効率化手法も導入している。今後の課題として著者らは、極端な計算予算下での最適化ダイナミクスの解明、およびより多くのモジュールを持つ大規模パイプラインへのスケーラビリティ検証を挙げている。DSPyフレームワークへの統合により、実務者がプロンプトエンジニアリングの手作業を削減し、データ駆動でプログラム全体を最適化する道が開かれている。

参考文献


この記事はAI(Claude)による論文解説です。内容の正確性には注意を払っていますが、詳細は原論文を参照してください。

この投稿は CC BY 4.0 でライセンスされています。

論文解説: GEPA — リフレクティブなプロンプト進化で強化学習を超える

論文解説: グラフ型ベクトル検索の包括的実験評価 — HNSW・DiskANN・RoarGraph・ACORNを同一条件で比較