本文へスキップ
株式会社JQITCorporate
Python機械学習ファインチューニング生成AILLM

【松尾研コンペ】非エンジニアのLLMファインチューニング記録 ── 前編

執筆:株式会社JQIT(技術広報)

東大松尾研の LLM 講座コンペで、構造化データの出力に特化したローカル LLM を作った記録の前編です。ファインチューニング未経験の状態から、合格ラインを超えるまでの過程をまとめています。

はじめに

昨年、松尾研のLLM講座を受講して初めてLLMの世界に触れました。今年は2年目。コンペも2回目の挑戦です。

今回のテーマは構造化データへの変換性能に特化したローカルLLMの開発。JSON、XML、YAML、TOML、CSVといった構造化データ(決まったルールに従って整理されたデータ形式)を正確に出力できるモデルを作ります。

「LLMの微調整なんてやったことないぞ…」という状態からのスタート。運営が指定するベンチマーク(LLMの性能をチェックする試験のようなもの)で高得点を目指すのですが、合格ラインは0.7。最初は0.68で不合格……そこから6回の実験を重ねて、ようやく0.73で合格ラインを突破できました。この記事では、その過程で得た知見を備忘録としてまとめます。

スクリーンショット 2026-02-06 23.21.52.png

コンペのルールでLLMによるデータセット生成は禁止されていますが、コーディングや分析での利用は問題ありません。

このコンペでは推論時のプロンプト(モデルへの指示文)は運営が指定したもので修正禁止です。つまり、プロンプトを工夫してスコアを上げる「プロンプトエンジニアリング」はできない。勝負どころはデータセット(学習に使うお手本データ)の選び方学習パラメータ(学習の速さや強さを決める設定値)の調整に絞られます。

CursorやClaudeを使い、まず固定された推論プロンプトの中身を一緒に読み解いて、「このプロンプトが求めている出力形式は何か」「どんなデータセットで学習すれば相性がいいか」を分析する。そこから学習パラメータの方針を相談して、実験結果が出たら出力を一緒に検証する──こんな感じで作戦を立てる「相棒」としてフル活用しています。初学者にとって、壁打ち相手がいるのは本当に心強いですね。

同じように初めてSFTに挑戦する方の参考になれば嬉しいです。

コンペの概要 ── 構造化データ変換って何?

このコンペでは、自然言語の指示に従って構造化データを正確に生成・変換できるLLMを作ります。

たとえばこんなタスクです。

  • 「以下のテキストからユーザー情報をJSON形式で抽出してください」
  • 「このXMLをYAMLに変換してください」
  • 「以下の条件でCSVデータを生成してください」

作ったモデルは運営が用意したベンチマークで採点されます。いわばLLMのための「試験」ですね。評価基準は構造の正確性(JSONの括弧が正しく閉じているか、キー名が合っているかなど)とキーワードマッチング(必要な値がちゃんと含まれているか)。フォーマットが壊れていたり、必要な項目が抜けていたりすると大幅に減点されます。

合格ラインは0.7。これを超えるのが最初の目標です。

使用するベースモデル

ベースモデル(微調整のベースとなる既存のLLM)も運営から指定されていて、今回は Qwen3-4B-Instruct-2507 です。「4B」はパラメータ(モデルの脳細胞のようなもの)が40億個という意味で、LLMとしては小型な部類。Google ColabのGPUで学習できるサイズ感です。それだけでなく、ローカルのPCでも動かせるくらい軽量です。実は普段から実験的にQwen3-4Bを使っていたりするので、わりと馴染みのあるモデルでした。

環境構築 ── Colab + Unsloth + QLoRA

技術スタック

項目

内容

実行環境

Google Colab Pro+(T4 / L4 GPU)

微調整ライブラリ

Unsloth

手法

QLoRA(4bit量子化LoRA)

学習フレームワーク

TRL(Transformers Reinforcement Learning)

各ツールをざっくり説明

ここで出てくる用語をざっくり解説しておきます。

**SFT(教師あり微調整)**は、お手本データを見せて「こう答えてね」とモデルに教える学習方法です。人間が問題集を解いて勉強するのに近いイメージですね。

Unslothは、このSFTを高速化するライブラリ。通常の2〜5倍速く学習できると言われています。

QLoRAは、メモリ節約の技術です。モデル全体を書き換えるのではなく、ごく一部だけを効率よく調整するLoRAという手法に加えて、モデルを4bitに圧縮(量子化)することで、少ないGPUメモリでも大きなモデルを扱えるようにしています。

TRLは、Hugging Faceが提供する学習フレームワーク。SFTの学習を簡単に実行できるトレーナーが入っています。

GPUは、もともとゲームの画像処理用チップですが、大量の計算を並列で高速処理できるため、AI学習にも使われています。LLMの学習にはGPUが必須です。

Colab環境の変遷 ── ブラウザ → VSCode拡張 → SSH接続

実は環境構築にもかなり試行錯誤がありました。

第1段階: ブラウザから利用(無料プラン)

最初はColabのブラウザUIでノートブックを実行していました。無料プランのT4 GPUで十分……と思っていたのですが、すぐにGPU不足に。学習を何度も回すとあっという間に使用制限に引っかかります。

第2段階: Pro+に課金 → VSCode拡張機能

これは厳しいということでColab Pro+に課金しました。L4やA100といった高性能GPUが使えるようになります。

せっかくならVSCodeから操作したいと思い、Google Colab拡張機能を導入。ところが、VSCode拡張機能からL4やA100のGPUに接続ができない。T4には繋がるのにL4/A100は選べない……原因不明です。誰かやり方知っていたら教えてください。

第3段階: SSH接続(現在)

最終的にCloudflaredトンネル経由のSSH接続に落ち着きました。これならVSCodeのRemote-SSH拡張から、L4でもA100でも問題なく接続できます。

ColabへのSSH接続手順

同じ悩みを持つ方のために、手順を残しておきます。

ローカル側の準備(初回のみ)

1. cloudflaredをインストール

brew install cloudflared

2. ~/.ssh/configに追記

Host *.trycloudflare.com
    HostName %h
    User root
    Port 22
    ProxyCommand /opt/homebrew/bin/cloudflared access ssh --hostname %h

Colab側のセットアップ(ブラウザで毎回実行)

Colabでランタイムタイプを選択(L4やA100など)した後、ブラウザのColab上で以下のセルを順番に実行します。

# セル1: SSHトンネル起動
!pip install colab_ssh --upgrade
from colab_ssh import launch_ssh_cloudflared
launch_ssh_cloudflared(password="任意のパスワード")

実行するとxxx.trycloudflare.comというホスト名が表示されます。これをコピー。

# セル2: SSH鍵ペアの生成
!ssh-keygen -t ed25519 -f /tmp/colabkey -N "" -q && cat /tmp/colabkey.pub >> /root/.ssh/authorized_keys && cat /tmp/colabkey
# セル3: sshd設定の修正(鍵認証を有効化)
!sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config
!sed -i 's/#AuthorizedKeysFile/AuthorizedKeysFile/' /etc/ssh/sshd_config
# セル4: authorized_keysのパーミッション設定 + sshd再起動
!mkdir -p /root/.ssh && cat /tmp/colabkey.pub >> /root/.ssh/authorized_keys && chmod 700 /root/.ssh && chmod 600 /root/.ssh/authorized_keys && service ssh restart
# セル5: 秘密鍵を表示(これをローカルに保存する)
!cat /tmp/colabkey

セル5で表示された秘密鍵をローカルの~/.ssh/id_colabに保存します。

# ローカルで実行
pbpaste > ~/.ssh/id_colab   # macOSの場合、コピーした鍵を貼り付け
chmod 600 ~/.ssh/id_colab

ローカルからSSH接続

ssh -i ~/.ssh/id_colab root@<表示されたホスト名>.trycloudflare.com

VSCodeならRemote-SSHで同じホスト名を指定すればOK。鍵認証なのでパスワード入力なしで繋がります。

注意: Colabのセッションが切れるとホスト名が変わります。再接続時はセル1のlaunch_ssh_cloudflaredから再実行してください。鍵ペアもセッションごとに再生成が必要です。

Colabでのセットアップ手順

ノートブックは大きく5ステップで構成しました。

  1. Step 0: Google Driveのマウント(超重要!
  2. Step 1: 依存関係のインストール(numpy, transformers, trl, unsloth等をバージョン固定)
  3. Step 2: HuggingFaceへのログイン
  4. Step 3: SFT学習の実行
  5. Step 4: 学習成果物の確認 → LoRAアダプタをHuggingFaceにアップロード

Step 0のGoogle Driveマウントは地味ですが超重要です。Colabはセッションが切れるとローカルのファイルはすべて消えます。学習データ、チェックポイント、LoRAアダプタ……せっかく何時間もかけた成果が全部パーです。Google Driveにマウントしておけば、セッションが切れても成果物が残ります。これは最初にやっておきましょう。

ColabではGPUの種類によって設定が変わります。L4 GPUの場合はbf16=True、T4の場合はfp16=Trueを指定します。これはGPUが得意な計算精度(数値の扱い方)の違いで、間違えるとエラーになるので注意してください。

データセット選びの試行錯誤

SFTで最も重要なのはデータセットの質だと実感しました。

なお、このコンペでは使用できるデータセットは運営から指定されたものに限定されています。著作権などの問題から、LLMで生成したデータを学習に使うのはNGです。ただし、スクリプトを書いてルールベースでデータをクリーニング・前処理するのはOK。この制約の中でいかに工夫するかがポイントになります。

指定されたデータセットは大きく2系統、全9種類ありました。全部で35,815行。ここからどれを選び、どう組み合わせるかが腕の見せどころです。

u-10bei系(CoTあり・6種類)

アシスタントの回答に**Chain of Thought(推論ステップ)**──つまり「こう考えました」という思考過程──が含まれるデータセットです。こんな形式ですね。

Approach:
1. Identify the required format...
2. Determine the schema...
...
Output:
{"name": "John", "age": 30}

5ステップの推論過程のあとに構造化データが出力されます。

1-1. u-10bei/structured_data_with_cot_dataset_512_v2 ★標準コードで使用

項目

内容

サイズ

3,930行(trainのみ)

CoT

5ステップ推論

出力形式

Approach → Output(コードのみ、フェンスなし)

フォーマット

JSON, XML, YAML, TOML, CSV

タスクタイプ

生成(generation)と変換(conversion)

複雑度

simple, medium, complex の3段階

URL

https://huggingface.co/datasets/u-10bei/structured_data_with_cot_dataset_512_v2

1-2. u-10bei/structured_data_with_cot_dataset_512_v4

項目

内容

サイズ

5,760行(train: 4,610 / val: 575 / test: 575)

特徴

最大行数、train/val/test分割あり。メタデータにprompt/outputフィールド追加

URL

https://huggingface.co/datasets/u-10bei/structured_data_with_cot_dataset_512_v4

1-3. u-10bei/structured_data_with_cot_dataset_512_v5

項目

内容

サイズ

5,680行(train: 4,550 / val: 568 / test: 568)

特徴

constraintフィールド追加(minified/sorted/null)。random_structured_dataスキーマあり

URL

https://huggingface.co/datasets/u-10bei/structured_data_with_cot_dataset_512_v5

1-4. u-10bei/structured_data_with_cot_dataset_512

項目

内容

サイズ

3,445行(trainのみ)

特徴

typeフィールドあり(conversion約60%/generation約40%を明示)。512トークン上限

URL

https://huggingface.co/datasets/u-10bei/structured_data_with_cot_dataset_512

1-5. u-10bei/structured_data_with_cot_dataset_v2

項目

内容

サイズ

2,500行(trainのみ)

特徴

typeフィールドなし。v1より長い回答。2.26MB

URL

https://huggingface.co/datasets/u-10bei/structured_data_with_cot_dataset_v2

1-6. u-10bei/structured_data_with_cot_dataset(オリジナル)

項目

内容

サイズ

2,500行(trainのみ)

CoT

4ステップ推論(v2より1ステップ少ない)

特徴

最も基本的なバージョン。1.9MB

URL

https://huggingface.co/datasets/u-10bei/structured_data_with_cot_dataset

daichira系(CoTなし・クリーン出力・3種類)

こちらはアシスタントの回答が構造化データのみ。余計な文章は一切なし、コードフェンスもなし。プロンプトにフォーマット固有の制約指示が含まれているのが特徴です(例: TOML「Do NOT use inline tables」)。

{"name": "John", "age": 30}

2-1. daichira/structured-3k-mix-sft

項目

内容

サイズ

3,000行(trainのみ)

フォーマット

JSON, XML, YAML, TOML, CSV(各600、完全均等)

タスクタイプ

extract(抽出)+ transform(変換)の2種類、17サブカテゴリ

特徴

5形式完全均等配分。TOML制約指示あり

URL

https://huggingface.co/datasets/daichira/structured-3k-mix-sft

2-2. daichira/structured-5k-mix-sft

項目

内容

サイズ

5,000行(trainのみ)

フォーマット

YAML多め(~30%), JSON/XML各20%, TOML/CSV各15%

タスクタイプ

extract + transform。toml_to_yaml, toml_to_json等の追加サブカテゴリ

特徴

最大行数。形式配分は不均等(YAML偏重)。TOML制約指示あり

URL

https://huggingface.co/datasets/daichira/structured-5k-mix-sft

2-3. daichira/structured-hard-sft-4k

項目

内容

サイズ

4,000行(trainのみ)

フォーマット

JSON, XML, YAML, TOML(CSV少なめ)

タスクタイプ

transform中心(深いネスト、多様な型の組み合わせ)

特徴

高難度特化。deterministic serialization使用

URL

https://huggingface.co/datasets/daichira/structured-hard-sft-4k

データセット全体比較

ID

データセット名

行数

CoT

出力スタイル

TOML制約

分割

1-1

structured_data_with_cot_dataset_512_v2

3,930

✅ 5step

Approach→Output

なし

trainのみ

1-2

structured_data_with_cot_dataset_512_v4

5,760

✅ 5step

Approach→Output

なし

train/val/test

1-3

structured_data_with_cot_dataset_512_v5

5,680

✅ 5step

Approach→Output

なし

train/val/test

1-4

structured_data_with_cot_dataset_512

3,445

✅ 5step

Approach→Output

なし

trainのみ

1-5

structured_data_with_cot_dataset_v2

2,500

✅ 5step

Approach→Output

なし

trainのみ

1-6

structured_data_with_cot_dataset

2,500

✅ 4step

Approach→Output

なし

trainのみ

2-1

structured-3k-mix-sft

3,000

データのみ

✅ あり

trainのみ

2-2

structured-5k-mix-sft

5,000

データのみ

✅ あり

trainのみ

2-3

structured-hard-sft-4k

4,000

データのみ

✅ あり

trainのみ

合計

35,815

どちらを使うべきか?

コンペのルールブックには「余計な文章を出さない(コードだけ出すのが安全)」と書かれていました。

ここで重要な比較です。

特徴

u-10bei系(CoTあり)

daichira系(CoTなし)

出力スタイル

推論 + データ

データのみ

ルールブック適合

△(余計な文章あり)

◎(クリーン出力)

読み取りやすさ

CoT部分の解析失敗リスク

そのまま安全に読み取り可能

TOML品質

制約指示なし

「Do NOT use inline tables」あり

結論から言うと、daichira系のみを使った実験(exp06)がスコア最高でした。CoTの推論ステップは一見賢そうに見えますが、出力に余計な文章が混入するリスクがあり、構造化データの正確性を求めるベンチマークでは不利に働いたのです。

実験の記録 ── 6回の試行錯誤

ここからは実際の実験結果を紹介します。試行錯誤の流れを追ってみてください。

実験1〜3: 学習率を上げれば性能は上がる?

最初の3実験では、同じデータセット構成で**学習率(Learning Rate)**を変えてみました。

学習率って何?
「お手本からどれくらいガツガツ学ぶか」を決める数値です。大きくすると一度にたくさん学ぶけど大雑把になりがち。小さくすると丁寧に学ぶけど時間がかかる。ちょうどいい値を見つけるのが重要です。

実験

学習率

練習中の成績(Val Loss)

本番の成績(スコア)

合格ライン(0.7)

exp01

1e-6(超ゆっくり)

1.3033

0.688

不合格

exp02

2e-6(ゆっくり)

1.0594

0.682

不合格

exp03

2e-5(速め)

0.5693

0.678

不合格

全部不合格。しかも面白い結果が出ました。

Val Loss(Validation Loss)って何?
「練習中の模擬試験の点数」のようなものです。値が小さいほど模試の成績は良い。ただし、模試で高得点を取っても本番の試験(ベンチマーク)で良い点が取れるとは限らないんです。ここがミソでした。

学習率を上げると模試の成績は上がるのに、本番の成績は下がるのです。

普通、模試の成績が良ければ本番もいけると思いますよね? でも違いました。

発見: 模試の成績 ≠ 本番の成績

原因を調べてみると、**速くガツガツ学びすぎて、出力が「過度に簡素化」**されていました。

たとえば「Text to CSV」タスクで10行必要なところ、exp03では1行しか生成しないケースが見つかりました。模試では「フォーマットは合ってる」から点が取れる。でも本番では「必要なデータが足りない」から大幅減点になるわけです。

学習率 1e-6(超ゆっくり)→ 模試 1.30 → 本番 0.688(合格ラインまであと0.012)
学習率 2e-6(ゆっくり)  → 模試 1.06 → 本番 0.682(むしろ下がった…)
学習率 5e-6(ふつう)    → 模試 0.46 → 本番 0.674(さらに下がった…!)
学習率 2e-5(速め)      → 模試 0.57 → 本番 0.678(全然ダメ)

結論: 学習率は1e-6(超ゆっくり)が最適。模試の成績だけ見ていてはダメ。

これは大きな学びでした。「たくさん勉強すれば成績が上がる」わけじゃなく、丁寧に学んだほうが本番に強いんですね。でも学習率をいじっているだけでは合格ライン0.7には届かない。別のアプローチが必要だと気づきました。

実験6: daichira系のみ + MAX_SEQ_LEN拡大 → スコア大幅改善!

最終的に最高スコアを叩き出したのがexp06です。2つの大きな変更を行いました。

変更1: daichira系データセットのみ使用

u-10bei系(CoTあり)を思い切って全部外し、daichira系3つだけで学習しました。合計12,000行。データ量は減りましたが、クリーンな出力だけを学習させるという戦略です。

変更2: MAX_SEQ_LENを512 → 1024に変更

正直なところ、なぜこれが効くのかは完全には理解できていません。ただ、変えてみたら点数が上がりました!

MAX_SEQ_LENって何?
「お手本データを何文字分まで読むか」の上限です。たとえば512だと、お手本が長い場合に途中でバッサリ切られてしまいます。読書感想文のお手本が途中で切れていたら、まともに学べないですよね。

512トークン(約500単語分)では、daichira系の長めの出力が途中で切れてしまい、全体の51.6%がお手本として使い物にならない状態になっていました。

1024に変更したところ、使えないデータが51.6% → 7.4%に大幅改善。有効な学習データ量が一気に増えました。

変更点

exp01

exp06

効果

データセット

CoT混在

daichiraのみ

余計な文章混入なし

お手本の読み取り上限

512

1024

使えないデータ 51.6%→7.4%

有効なお手本の数

~5,973件

9,885件

+65%増

本番スコア

0.688(不合格)

0.737(合格!)

+7.2%改善

学習率は1e-6のまま。データの質と量を改善しただけで、スコアが0.688 → 0.737と大きく伸び、ついに合格ラインの0.7を突破しました!

最終結果と学んだこと

6回の実験を通じて得た知見をまとめます。

全実験結果の比較

実験

データセット

学習の速さ

模試の成績

本番スコア

判定

exp01

u-10bei×3 + daichira×1

超ゆっくり

1.3033

0.688

不合格

exp02

+hard追加(5つ)

ゆっくり

1.0594

0.682

不合格

exp03

同上

速め

0.5693

0.678

不合格

exp04

重複除去+フィルタ

ふつう

0.4631

0.684

不合格

exp06

daichira×3のみ

超ゆっくり

0.7057

0.737

合格!

3つの重要な学び

1. データセットの質 > 量

データ量を増やすことよりも、タスクに適した「クリーンな」データを使うことのほうが効果的でした。CoT(推論ステップ)付きのデータは一見リッチに見えますが、構造化データの正確な出力が求められるタスクでは、余計な文章が混入するリスクになりました。

2. 模試の成績だけ見ていてはダメ

練習中の模試(Val Loss)の成績が良くても、本番(ベンチマーク)の成績が逆に下がることがあります。ガツガツ学びすぎると出力が簡素化されて、必要なデータが欠落してしまうのが原因でした。最終的な本番の評価でモデルを判断することが大切です。

3. お手本の読み取り上限(MAX_SEQ_LEN)はデータに合わせて設定する

デフォルトの512トークンでは、お手本データの半分以上が途中で切れて使い物にならなくなっていました。お手本の実際の長さを確認して、上限を適切に設定するだけで、有効データ量が65%も増加しました。これは見落としがちですが、影響が非常に大きい設定値です。

まとめ

LLMの微調整は初めてでしたが、実験を繰り返すことで少しずつコツが掴めてきました。

特に印象的だったのは、直感に反する結果が出ることです。「たくさん勉強すれば成績は上がるはず」「お手本は多いほど良いはず」── こういった思い込みが、実験で覆されました。

コンペはまだ続いているので、引き続き改善を重ねていきます。この記事が、同じようにLLMの微調整に初めて挑戦する方の参考になれば幸いです。


この記事は、松尾研LLM講座のコンペティションに参加した際の備忘録です。
後編: LLMファインチューニング5つの失敗と逆転の一手

この記事は Qiita でも公開しています。

この記事のような仕組みを、業務に合わせて設計・開発しています。

AIエージェント開発・RAG構築の支援内容を見る

同じような課題をお持ちですか。

現状の整理からご相談いただけます。まずはお気軽にお問い合わせください。