LayerX エンジニアブログ

LayerX の エンジニアブログです。

Repeated Samplingを使ったLLM推論時スケーリングで麻雀点数計算問題生成タスクを解くぞ!

LayerX バクラク事業部で AI/MLOpsエンジニアをしている中村(@po3rin)です。そしてこの記事はLayerX AI Agentブログリレー42日目の記事です。2ヶ月以上続いています。そろそろ狂気じみてきましたね。

今回はLLMによる麻雀点数計算問題生成タスクをRepeated Samplingでタスク成功率を上げた話をします。また、今回Repeated Samplingを実装した経験から、Repeated Samplingを実装する際の重要な観点などもお伝えします。

目次

Repeated Samplingとは

Repeated SamplingはLLM推論時スケーリングの一種です。学習時だけでなく、推論時にも計算量を増やすことで、モデルの性能をさらに引き出そうというアイデアの元、LLMに大量に候補を生成させ、その中からVerifierが最終回答として判定したものを最終回答とする手法です。

arxiv.org

下図はRepeated Samplingの流れを示したものです。

論文からの引用。Repeated Samplingの概要図

Repeated Samplingによってタスク成功率があがるのはもちろんですが、弱いモデルから生成した多くのサンプルが高性能なモデルからのシングルサンプルを上回る可能性があることも示し、費用対効果が高くなることも言及しています。

以降の話で重要になるポイントをかいつまんで説明します。

Repeated Samplingの有効性は、次の2つの特性に依存します。

  • Coverage: 生成された複数の候補の中に、少なくとも1つの正解が含まれる割合。サンプル数を増やすことで、このカバレッジがどれだけ向上するかを評価します。
  • Precision: 生成された候補の中から、タスク成功を正確に識別できる能力。カバレッジが高くても、正解を見つけ出す能力がなければ、実用的な性能は得られません。

Precisionを決めるのはVerfierの実装です。Verifierの実装はタスクの種類によって難易度が変わります。コーディングタスクのユニットテストのように、タスクの成否が決定論的に検証できるツールがすでにある場合、Precisionは100%なので、Coverageの向上がダイレクトにタスクの成功率に繋がります。逆に検証できるツールがなく、VerifierのPrecisionが低い場合、Coverageが高くても、その中から成功タスクを見つけることができません。そのため、Verifierの実装は非常に重要です。

論文の詳細は長くなるので紹介しませんが、推論時のスケーリング則について様々な観点から議論されているので、気になる方はぜひ論文をご覧ください。

ちなみに、LLM推論時スケーリングの中でもRepeated Samplingの発展として、AB-MCTS(Adaptive Branching Monte Carlo Tree Search)という手法もあります。

arxiv.org

AB-MCTSは多段階の探索と活用を可能にします。探索木における各ノードで、外部フィードバック信号に基づいて、新しい候補を生成して「より広く探索する(go wider)」か、既存の応答を再検討して「より深く探索する(go deeper)」かを動的に決定します。

論文から引用。AB-MCTSの概要図

この辺りはSakanaAIさんの資料もとても良いので、そちらをご覧ください。私はとある週に別々の人から3回くらいこの資料を紹介されました笑。それほど社内でこの資料が出回りました。

speakerdeck.com

今回はRepeated Samplingで麻雀点数計算問題生成タスクを解いていきます。

麻雀点数計算問題生成タスクとは

みなさんお馴染み麻雀点数計算問題生成タスクです。前回はLLMが苦手な麻雀点数計算問題生成タスクの精度をMulti Agent構成で33%から90%に上げた話をしました。この際にはシンプルな指示(N飜M符が答えになる問題を作成して)だけに絞った実験でした。

tech.layerx.co.jp

簡単におさらいします。前回は次のようなMulti Agent構成を採用しました。

  • phase1: 麻雀点数計算問題を生成
  • phase2: 点数があっているかをtoolでチェック、間違っていたら修正ループを回す。
  • phase3: 最終的な評価のために、問題文を構造化出力する。期待する構造化出力ができていなかったら修正ループを回す。

前回取り組んだMulti Agentパターン

この構成でGemini-1.5-flashでタスク成功率90%を達成しました。しかし、上の取り組みでは次の課題がありました。おさらいしましょう。

  • 精度がまだ十分ではない(修正ループ回数制限内に修正しきれなかった)

    • 麻雀のルールをずっと間違えて認識したまま点数計算問題を修正し続ける
    • 構造化出力で失敗する
    • シンプルな指示しか対応していない(「〇〇の役を含みXX点になる問題を出して!」といった複数条件)
  • 結果が返ってくるまで遅い

    • 複数回のループを挟むので非常に遅い

精度に関しては現状でも、20サンプルで90%を達成していますが、この機能をアプリケーションの機能として提供する場合、指定した問題自体が間違っているのは致命的です(点数計算を学びたい人の学習を妨げてしまう恐れがあります)。

また、上のアーキテクチャでは最終出力を得るまでに非常に時間がかかります。一問を生成するのに大体100秒程かかります。このアーキテクチャはAB-MCTSの論文でも語られているSequencial Refinement Patternであり実行時間がかかるという問題があります。また、最終解答の精度の問題もあります。Sequencial Refinement Patternは与えられた初期解や特定の改善パスに固執する傾向があり、もしその初期解が最適でない場合や、改善の方向性が誤っている場合に、他の有望な探索空間を効果的に見つけ出すことが難しいです。

そこで今回は、Repeated Samplingを使うことによって実際に点数計算関数の入力に合う出力がどれくらい出せるかを実験しました。

Repeated Samplingの実装前に確認すべきこと

Repeated Samplingの実装とポイントについてお話しします。私の経験上、Repeated Samplingを実装する前に次のステップをお勧めします。

  • Verifierの実装可能性の検討
  • ざっくりCoverageの確認

Verifierの実装可能性の検討

Repeated Samplingするにあたり、Verifierの実装が可能か?という問いはとても重要です。なぜならば、先ほど紹介したように、VerifierのPrecisionが頭打ちだと、高いCoverageの恩恵を十分に受けられないからです(サンプルの中に正しいものがあっても、それを選びとれなくては意味がない)。

pass@kが良くても、Precisionが低いと意味がない。。。

麻雀点数計算問題生成タスクに必要なVerifierの特性を考えます。今回のタスクにおいては、LLMが出力した点数計算問題(自然言語)が、ユーザーの指示に沿った問題かを判定するVerifier実装が必要です。点数計算部分は決定論的に可能であり、そのようなPythonモジュール(mahjong)も公開されていますが、自然言語の点数計算問題から点数計算に必要な情報をLLMで抽出する処理と、その情報と指示が一致しているかをLLMで判定する部分が非決定論的です。そのため、今回のタスクにおいてPrecisionはそのまま構造化出力とLLM-as-a-Judgeの精度となります。構造化出力は色んな手法がある(BAMLやOpenAIのStructured Output)ので、ある程度精度を上げれることができそうです。また、LLM-as-a-Judgeにおいては点数計算結果と比較するだけ(自然言語の指示と突き合わせる必要がある)なので、それほど難しいタスクではありません。よってPrecisionが高いVerifierを実装することは可能であると判断しました。

このようにRepeated Samplingが使えるかはPrecisionの高いVerifierが用意できるかが重要なポイントになります。

ざっくりCoverageの確認

ざっくりCoverageを確認します。ここが分からなければ、サンプル数をどれくらいにしないと期待するタスク成功率が出せるか分からないからです。決定論的に検証可能なタスクの場合、Coverageはすぐに確認できますが、そうでないタスクの場合は、とりあえず手で動かしてみて、どれくらいのCoverageが出そうかを自分の目で確認する必要があります。

ざっくりのCoverageが低すぎる場合、Repeated Samplingで必要なサンプル数が大きくなってしまいます。そうすると、プロバイダーが提供するLLMを利用する場合、料金コストが大きくなります。

私の場合は5指示でどれくらいのCoverageが出せそうかを指示を変えながら何回か叩いて確認しました。今回のタスクの場合、GPT-5による候補生成(指示5個、サンプル数5)でCoverageは90~100%近くになることを確認しました。

このようにRepeated Samplingが予算内で必要な成功率が出せそうかをざっくり確認します。また、この確認の利点として、実験時に必要なサンプル数のあたりをつけることができる点も挙げることができます。

Repeated Samplingの実装

Repeated Samplingが使えそうであることをPrecision、Coverageの観点から確認できたので早速実装していきます。

BAMLを使ったVerifier実装

Repeated Samplingするにあたり、Verifierの実装はとても重要です。なぜならば、先ほど紹介したように、VerifierのPrecisionが頭打ちだと、高いCoverageの恩恵を十分に受けられないからです(サンプルの中に正しいものがあっても、それを選びとれなくては意味がない)。

今回のタスクである麻雀点数計算問題生成におけるVerifierの特性を考えます。点数計算部分は決定論的に可能ですが、今回のタスクにおいては、LLMが自然言語で出した点数計算問題が指示に沿った問題かを判定するVerifier実装が非決定論的な処理になります。なぜなら、自然言語の点数計算問題から点数計算に必要な情報をLLMで抽出する必要があるからです。

よって点数計算問題生成タスクをRepeated Samplingで解く場合、このVerifier実装が非常に大事です。今回はBAMLを使った構造出力を行います。その出力を使って、点数計算と役の取得を行い、LLMで点数一致と、指示適合を確認します。いわゆるLLM-as-a-Judgeです。

麻雀点数計算が指示にあっているかを判定するVerifier実装

Verifierを一つのAgentとして構造化、点数計算ツール利用、判定をステップとして1つのプロンプトにまとめることもできますが、今回はコントローラブルにするためにAgentic Workflowとして実装しています。これにより、確実に構造化、点数計算、判定が行われるようにしています。Verifier実装はどれだけ決定論的にできるかが勝負です。

今回BAMLによる構造化出力をしたのは、麻雀のような複雑なルールを形や規約で表現できるためです。麻雀点数計算問題は次のデータを抽出しなくてはいけません。

  • 手牌
  • アガリ牌
  • ドラ
  • 鳴き情報
  • 場風、自風
  • 親、子
  • アガリ方(ロン/ツモ)

さらに各種データには麻雀ルールに基づいた制約があります。

  • 手牌は1~9m(萬子), 1~9p(筒子), 1~9s(索子)、1~7z(字牌)で表現
  • 手牌はアガリ牌を含め、カンがある場合は、その数だけ、手牌リストが伸びる。
  • 鳴いた牌は全て手牌リストに含まれてなくてはいけない。

これらをプロンプトだけで制約を持たせるのは困難です。そのため、構造化出力を矯正する何かしらの仕組みが必要です。そこで、今回はBAMLを採用しました。

BAMLはBAMLファイルにPython Likeに型を定義し、そこからPydanticコード自動生成できます。さらに、BAMLファイルさえあれば他の言語(Go、Ruby、JavaScript)などのコードも生成可能です。チーム内で使い回す構造化Agentの開発に非常に便利です。

BAMLファイル全てを貼ると長くなってしまうので、ポイントだけお伝えします。BAMLは利用するLLMモデル、構造化プロンプト、データの型を記述し、コード自動生成することで、構造化出力Agentを生成することが出来ます。

class Hand {
// ... 省略
}

// OpenAI GPTクライアント設定
client<llm> GPT4oMini {
  provider openai
  options {
    model "gpt-4o"
    api_key env.OPENAI_API_KEY
  }
}

// 自然言語の問題文から手牌データを構造化抽出する関数
function ExtractHandFromQuestion(question: string) -> Hand {
  client GPT4oMini
  prompt #"
    以下の日本語の麻雀問題文から手牌情報を抽出し、完全なJSONオブジェクトとして出力してください。
//...(省略)

例えば、麻雀牌をの表現を次のように制限できます。

// 牌の種類を文字列型で定義
// aliasが効かないため、直接文字列として定義する
// 有効な値: "1m"〜"9m", "1p"〜"9p", "1s"〜"9s", "1z"〜"7z"
// m, p, s, z はそれぞれ萬子、筒子、索子、字牌を表す
type TileType = string @assert(valid_tile, {{
  this|regex_match("^[1-9][mps]$") or this|regex_match("^[1-7]z$")
}})

アガリ形の手牌は14枚ですが、カンがあるとその回数分だけ手牌が増えます。そのため手牌は14~18枚であり、これを@assertで表現できます。

class Hand {
  tiles TileType[] @description("手牌の牌リスト。和了牌(win_tile)を含む。鳴いた牌も全て含める。カンがない場合は正確に14枚、カンがある場合はカン1つにつき1枚追加(15-18枚)。形式: '1m', '2p', '3s', '1z'など")
    @assert(tile_count, {{ this|length == 14 or this|length == 15 or this|length == 16 or this|length == 17 or this|length == 18 }})

これらのルールをプロンプトで全て強制することは難しいのですが、BAMLで強制することにより、出力がこの形に矯正されます。

BAMLファイルができたら次のコマンドでPythonコードを自動生成できます。

uv run baml-cli generate

これだけで、構造化出力Agentができます。簡単ですね。

20指示でVerifierがどのように動作するかを実験します。指示の例は次です。

タンヤオとピンフの両方を含む問題を作成してください
役牌が一つあり、答えが2000点になる問題を作成してください
暗刻が二つあり、3翻30符の問題を作成してください

//...(省略)

ここで補足ですが、前回のブログでは「N翻M符の問題を作成してください」という指示のみを扱うタスクでしたが、今回の実験では指示のバリエーションを増やしています。そのため、前回のMulti Agentの実験時よりもタスクの難易度はかなり高く設定しています。

これらの指示をプロンプトとともに、LLMに渡し、生成された問題文をBAMLで生成したAgentに渡して構造化します。そのデータを使い、Pythonライブラリであるmahjongを使った点数計算、LLM-as-a-Judgeに流します。

上の実装で、VerifierのPrecisionを検証していきます。GPT-5に生成させた問題をGPT-4を使ったVerifierに判定させています。問題生成だけをGPT-5に任せたのは、Verifierの挙動だけにフォーカスするために、問題を精度の良い物にしたかったからです(小さなモデルでは問題生成にほとんど成功しない)。

サクッと5個のサンプルスイートでVerifierの動作を確認した結果は次のようになりました。

============================================================
検証統計:
============================================================
総問題数: 5
指示通りの問題: 2
指示通りではない問題: 3
正しい候補を生成できた率: 40.0%

問題生成 -> Verifierで5件中3件が正しいと判定ができており、実際に2件の指示にあっていない問題を弾けていました。VerifierのPrecisionは100%です。これであればサンプル数は5個もあれば余裕そうです。これをRepeated Samplingで上げていきます。

LLMの並列推論で候補生成

Repeated Samplingの核である候補生成部分を実装していきます。

おさらいですが、Repeated Samplingは次のステップで動作します。

  • 一つの指示に対して、LLMを並列で走らせ、回答候補を複数生成する。
  • 候補に対してverifierで適合したものを見つけ、それを最終回答とします。

Verifier実装はすでにあるので、1つの指示に対して、LLMを並列で走らせ、回答候補を複数生成する部分を実装します。回答候補に対してverifierが適合とみなしたものをランダムに一つ選択して、それを最終回答とする実装をします。

実装ではシンプルな方式を採用しました。次のレベルで並列化を入れてます。

  • Candidates生成フェーズ: 候補生成は並列に
  • Verifierフェーズ : Candidatesに対して並列でVerifierの検証をかける

しかし、ここは別の実装方式も可能です。Candidatesを集め切るのを待つのではなく、Candidateができたら、ノンブロッキングでVerifierを走らせ、もし成功していたら、他の並列で走らせていたCandidate生成を打ち切る実装も考えられます。しかし、この方法は論文で述べられている次のような方式を利用できなくなります。

  • Majority Vote(多数決)方式
    • 生成された回答の中で最も頻繁に出現する回答を最終解とする。
  • Reward Model + Best-of-N方式
    • 報酬モデルを使って各候補の質をスコアリングし、最も高いスコアの解を最終解とする。
  • Reward Model + Majority Vote方式
    • 報酬モデルのスコアで各候補を重み付けし、重み付けされた多数決で最終解を決定します。

今回の実装では 候補単位で成否が明確に判定可能なタスク(pass-fail tasks)であるため、上のような複雑な方式は必要はありません。Future WorkとしてこのノンブロッキングVerifierパターンには取り組んでみたいと考えています。

実験

次の設定で実験を行いました。

  • 問題生成モデル: GPT-5
  • Verifier内で利用するモデル: GPT-4o
  • BAMLクライアント: GPT-4o
  • 指示数: 20
  • サンプリング数: 5
  • 評価メトリクス: タスク成功率(今回は私が目で本当にあっているかをチェックします。私はリアル麻雀で年間200半荘打つという訓練を受けているので心配は無用です)

実行結果は次のようになりました。

============================================================
全体の統計:
============================================================
総指示数: 20
成功: 19 (LLM-as-a-Judgeが適合と判断した候補が1つ以上見つかった指示の数)
失敗: 1 (LLM-as-a-Judgeが適合と判断した候補が1つも見つからなかった指示の数)
成功率: 95.0%

全体の成功率:
BAML抽出成功率: 100.0%
点数計算成功率: 98.0%
LLM-as-a-Judge実行: 98
指示通りだった候補数: 75
LLM-as-a-Judge成功率: 76.5%

小さなサンプルスイートですが、前回のブログで行った実験よりも複数条件が絡む難しい指示を含むタスクで成功率90%を達成しました。ちなみにRepeated SamplingなしのGPT-5単体のタスク成功率=全体のLLM-as-a-Judge成功率とみると76.5%でした。

個別の指示の結果をもう少し詳細に確認していきましょう。例えば「三色同順とタンヤオの両方を含み、答えが8000点になる問題を作成してください」という指示に対して、次に示すように生成された候補の成功率は80%でした。

============================================================
指示内容: 三色同順とタンヤオの両方を含み、答えが8000点になる問題を作成してください
============================================================

候補 1: ✓ BAML抽出成功, ✓ 計算成功 (点数: 8000, 役: Pinfu, Tanyao, Sanshoku Doujun, Dora)
  指示適合性: No
  理由: 役リストに「Pinfu」が含まれており、「三色同順」と「タンヤオ」の両方が含まれているが、指示には「Pinfu」は含まれていないため、指示に完全には従っていません。
候補 2: ✓ BAML抽出成功, ✓ 計算成功 (点数: 8000, 役: Riichi, Tanyao, Iipeiko, Sanshoku Doujun)
  指示適合性: 回答形式: Yes
  理由: 役リストに「Sanshoku Doujun」(三色同順)と「Tanyao」(タンヤオ)が含まれているため、役の条件は満たしています。しかし、指示に「答えが8000点になる」とありますが、実際の計算された点数は8000点であるため、点数の条件も満たしています。したがって、指示に明記されている条件はすべて満たされています。したがって、回答は「Yes」となります。
候補 3: ✓ BAML抽出成功, ✓ 計算成功 (点数: 8000, 役: Tanyao, Sanshoku Doujun, Dora)
  指示適合性: 回答形式: Yes
  理由: 役リストに「Sanshoku Doujun」と「Tanyao」が含まれているため、役の条件は満たしています。しかし、指示に「答えが8000点になる」とあるため、点数も条件に含まれます。実際の点数は8000点であり、条件を満たしています。したがって、指示に明記されている全ての条件が満たされているため、正しい回答は「Yes」です。
候補 4: ✓ BAML抽出成功, ✓ 計算成功 (点数: 4000, 役: Tanyao, Sanshoku Doujun)
  指示適合性: No
  理由: 役リストに「Tanyao」と「Sanshoku Doujun」が含まれているため、役の条件は満たしていますが、指示に「答えが8000点になる」とあるのに対し、実際の点数は4000点であるため、点数の条件を満たしていません。
候補 5: ✓ BAML抽出成功, ✓ 計算成功 (点数: 8000, 役: Pinfu, Tanyao, Iipeiko, Sanshoku Doujun)
  指示適合性: Yes
  理由: 役リストに「Tanyao」と「Sanshoku Doujun」が含まれており、指示に明記されている条件を満たしています。点数が8000点であることも確認されています。

注意すべきは「候補1のケースは正しい問題である」ということです。他の候補はVerifierが正しく判定できています。Yesと予測したものが3件でそのうち3件とも合ってるのでPrecisionは100%、正解が4つのうち3つだけを正しく予測してるので、Recallが75%、候補抽出のPrecisionが5個の候補に対して4つ正解を出してるので80%です。

GPT-5による候補生成の成功率が80%と高いため、Verifierが誤って正しくないと判断するケース(偽陰性)は許容できます。

Verifierの失敗への対策としてはプロンプトの修正や、もっと大きいモデル(GPT-5など)を使うなどが挙げられます。しかし、そもそもGPT-5の候補生成の精度が最初から高いため、今回はGPT-4を使ったVerifier実装でも問題なく、最終結果の精度が出ています。

追加の実験として、もう少しコストが下げられるかを検討します。PrecisionもCoverageも高い一方で、GPT-5による候補生成は高額のため、GPT-4による候補生成で実験します。

============================================================
全体の統計:
============================================================
総指示数: 20
成功: 1 (LLM-as-a-Judgeが適合と判断した候補が1つ以上見つかった指示の数)
失敗: 19 (LLM-as-a-Judgeが適合と判断した候補が1つも見つからなかった指示の数)
成功率: 5.0%

全体の成功率:
BAML抽出成功: 99
BAML抽出成功率: 99.0%
点数計算成功: 68
点数計算成功率: 68.7%
LLM-as-a-Judge実行: 68
指示適合候補: 1
LLM-as-a-Judge成功率: 1.5%

GPT-4だと成功率は5%に落ちてしまいました。。。この場合は、Coverageが低すぎるので、プロンプトやモデルを変更するか、サンプル数を上げるかを検討する必要があります(GPT-5すごいな。。。)。

Future Work

将来的に取り組むべき課題が残っています。例えば、成功タスクを発見次第、他のLLM推論をCancelする処理を実装すれば、出力トークン数を削減し、コスト削減に繋ぐことが出来ます。また、本記事で紹介したAB-MCTSを採用することにより、Refinementも取り入れたLLM推論時スケーリングを行うことが出来ます。これで、より小さなモデルで高い成功率を目指せるかもしれません。

また、今回はVerifierが指示に適合したものをランダムに選び取っていますが、指示内に「難しい問題」などの条件がある場合、候補の名からか一番難しいものを選び出す実装も考えられます。その場合はReward ModelやMajority Voteなどの方式で候補の中から選び取る実装も必要です。

まとめ

今回はLLMによる麻雀点数計算問題生成タスクをRepeated Samplingでタスク成功率を上げた話をしました。

Repeated Samplingは最初に知ったときは非常に簡単に実装できるように感じましたが、実際に手を動かしてみると、様々な学びがありました。Repeated Samplingが有効かどうかはPrecisionとCoverageに依存しているため、実装前に実験を行い、これらを確認しておく必要があります。また、タスクによってはVerifierの実装が困難だったりします。Precisionの高いVerifierの実装は腕の見せ所でしょう。

今回のように論文では簡単そうに見える手法でも、いざ、自分たちのドメインの問題を解かせようとすると、一筋縄ではいかないことがあります。そこが難しくもあり、楽しいポイントだなと改めて感じました。

この記事はLayerX AI Agentブログリレーの42日目の記事です。毎日AI Agentに関する知見をお届けします!!LayerXテック公式Xか僕のX(@po3rin)を是非フォローして見逃さないようにお願いします!

LayerXでは、「業務の完全自動運転」を実現するために、LLMをどのように活用していくかを追求するAIエンジニアを絶賛募集中です!!是非カジュアルにお話ししましょう。

ここから僕につながります! jobs.layerx.co.jp