0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FT-LLM 2026 チーム・ビクトリー 解法 推論パート

0
Last updated at Posted at 2026-03-27

はじめに

先日、言語処理学会第32回年次大会(NLP2026)内のワークショップの一部として、コンペティション「大規模言語モデルのファインチューニング技術と評価」が開催されました。

このワークショップでは、LLM-jpが開発した "llm-jp-4-8b" というモデルをベースに、数学タスクでの推論能力を向上させる課題と、自由形タスクによる応用事例の拡大を目的とした課題の2種類の課題が課されました(本記事執筆時点では llm-jp-4-8b は未公開)。参加者はいずれかの課題を選択して、個人またはチームで挑戦を行いました。私は全国各地から有志で集まった人達によって構成されるチーム、「チーム・ビクトリー」のメンバーとして、数学タスクでの推論能力を向上させる課題に参加し、チーム内では主に推論手法の実装や検証を行っていました。

チーム内で様々なことに挑戦した結果、私が所属する「チーム・ビクトリー」は、最終的に正解率84.7%(運営によるテストデータでの検証)を達成しました。

この記事では、我々のチームの解法について、主に推論手法の観点から報告したいと思います。

適用した推論方法

適用した推論方法は Self-Consistency と呼ばれる手法です。所謂多数決ですね。
Self-Consistency(以降、SC)は Wang らによって 2022 年に提案され、ICLR2023 に採択された LLM の推論手法の1種です。

Self-Consistency の概要

この手法をざっくりとまとめると、

  1. LLMに同じ問題を Chain-of-Thougnt(CoT)で$n$回解かせる
  2. 各解答を集計し、最も多い解答をその問題に対する最終解答とする

です。
以下の図(原論文 Figure 1 より該当部分のみを切り出し)を用いて具体的に見ていきます。

SC-概要.png

この図ではある問題に対して、CoT で推論された結果を元に次の2種類3個の解答パターンが示されています。

  • The answer is $18.(2回)
  • The answer is $26.(1回)

これを見ると、3個中2個が "18" となっているので、この問題に対する LLM(図中では Language Model)の解答は "18" となります。
このように、1つの問題に対して複数個の解答を生成させ、その中から最も出現頻度の高い解答パターンを最終解答とする手法が Self-Consistency です。これが多数決のように見えるので、Majority-Voting とも呼ばれています。

実装の流れ

では、提出した推論プログラムを元に、実装の流れを説明します。なお、本記事では解説に必要な箇所のみを抽象化したうえで記載しています。推論コード全体を知りたい方は以下のチームリポジトリの "inference/self-consistency.py" を参照してください。チームリポジトリには推論コード以外に、学習コードや評価コード、その他試行錯誤の結果もあります。合わせてどうぞ。
現時点ではベースモデルが公開されていないため、開発コード等の公開を控えております。ベースモデルである llm-jp-4 シリーズが正式に公開され次第、我々のチームも GitHub と Hugging Face 上で公開する予定です。

SC の実装は大きく分けて以下のパートに分けられます。

  1. 解答の生成
  2. 数式部分の抽出
  3. 多数決の実施

1. 解答の生成

モデルの呼び出し

解答の生成は、vLLM の LLM クラスを使用しています。

self-consistency.py
from vllm import LLM

llm = LLM(model=str(args.model_path.resolve())

引数 "model" に使用したいモデルのパスを指定します。今回はコマンドライン引数で指定できるようにしています。

出力生成

SC では、多様な解答パターンを得るために、LLM の出力時に指定する温度定数 "temperature" を高い値に指定します。temperature は 0.0 ~ 1.0 の範囲で指定し、0 に近いほど出力は固定され、逆に 1 に近いほど出力がばらつくようになっています。検証の結果、我々のチームでは 0.7 が最適だろうという結論に至りましたが、他チームの解法を聴講していると様々な値を取っていました。そのため、ベストプラクティスは存在せず、地道な検証で最適解を探索する必要がありそうです。

temperatureを筆頭に、出力生成のためのハイパーパラメータは、vLLM に含まれる SamplingParams クラスで設定が可能です。実際に出力を生成する際に、入力トークンと合わせて SamplingParams クラスのインスタンスを渡します。

self-consistency.py
from vllm import SamplingParams
sampleing_params = SamplingParams(
  temperature=args.temperature,
  max_tokens=args.max_tokens,
  top_k=40
)
final_outputs = llm.generate(input_ids, sampling_params=sampling_params)

Self-Consistencyでは、この処理を for ループで任意の回数 $k$ 回実施します。推論回数も要探索のハイパーパラメータですが、大体 40 回程度で正解率は収束する傾向にありました。ですので、一旦は$k=40$を目安に探索すると良いと思います。

ちなみに、我々のチームは推論時間ギリギリを攻めたかったので、欲張って$k=160$で提出しました。

2. 数式部分の抽出

数式部分の抽出には Hugging Face が公開している math-verify というライブラリの parse 関数を使いました。

math-verify を Python で利用する際は、pip でライブラリをインストールすることができます。

pip install math-verify

また、python コード内では以下の記述で parse 関数の import ができます。

self-consistency.py
from math_verify import parse

parse 関数は、テキストを入力することで、テキストの$LaTeX$ の数式記法内の部分を抽出し、math-verify の内部表現と合わせて返却します。つまり、

  • 引数: 任意のテキスト(str型)
  • 返り値: [math-verifyの内部表記, 引数で与えたテキストの数式部分)]
    • 例: ["2*x", "2x"]

の形でテキスト内の数式部分を parse してくれます。

vLLMの出力と合わせると以下のようになります。

self-consistency.py
...
# outputs: vLLMのLLMクラスによる出力
# 答えを抽出
extract_contens = [parse(output.outputs[0].text) for output in outputs]
...

多数決処理を行う際は、$LaTeX$ 記法による表記ゆれの影響をなくしたいため、math-verify の内部表現を利用しています。しかし、これをそのまま最終解答とすると、運営から提供された正答率計算のプログラムでは、実際には正解とされる問題も不正解とされてしまうため、最終解答は $LaTeX$ 記法にする必要があります。そのために、math-verify の内部表現と実際の $LaTeX$ 記法を対応付ける辞書を作り、集計し終えたタイミングで元の記法に戻しています。

3. 多数決の実施

2 で取得した解答から、最も出現頻度の高い解答を取得します。解答の集計は collenctions ライブラリの Counter クラスを使いました。前節までの内容+$\alpha$で各問題に対する解答を得ています。データの形状としては、[ 問題数, 推論回数 ] です。このデータに対して、Counterクラスで最多頻度の解答を決定します。集計処理自体は most_common メソッドで容易に実現可能でした。

self-consistency.py
from collections import Counter

# all_output: [問題数, 推論回数]のリスト
# all_output[i]にはi番目の問題の推論結果が
# [1回目の解答, 2回目の解答, ...,]のリストで保存されている
final_outputs = []
for outputs in all_outputs:
  if outputs:
    # 頻度順にすべての要素を取得(例: [('5', 3), (None, 2), ('4', 1)])
    ranked_answers = Counter(outputs).most_common()
            
    found_valid = False
    for answer, count in ranked_answers:
        # Noneではない最初の解答を探す
        if answer is not None:
            final_outputs.append(solution_dict[f"{answer}"][-1])
            found_valid = True
            break
    # 全てのサンプルがNoneだった場合のフォールバック
    if not found_valid:
      final_outpus.append(None)
  else:
    final_outputs.append(None)

おわりに

この記事では、LLMのファインチューニングコンペに対する解法の内、推論手法についてまとめました。このコンペを通じて、私自身、LLMに関する知見が深まったり、チームで何かしらの目標に向かって挑戦するという経験ができました。それに加えて、およそ半年ほど、つよつよな方に囲まれる環境に身を置けたことも良かったです。(松尾研の LLM コンペと合わせたら1年ですね)

あと、はじめて言語処理学会の年次大会に参加しましたが、活気の強さに圧倒されました。ワークショップ前日午後に会場入りしたので宇都宮観光はできませんでしたが、会場付近は楽しめましたと思います。またいつか、今度は正式な学会発表で参加したいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?