はじめに
昨年、松尾研のLLM講座を受講して初めてLLMの世界に触れました。今年は2年目。コンペも2回目の挑戦です。
今回のテーマは構造化データへの変換性能に特化したローカルLLMの開発。JSON、XML、YAML、TOML、CSVといった構造化データ(決まったルールに従って整理されたデータ形式)を正確に出力できるモデルを作ります。
「LLMの微調整なんてやったことないぞ…」という状態からのスタート。運営が指定するベンチマーク(LLMの性能をチェックする試験のようなもの)で高得点を目指すのですが、合格ラインは0.7。最初は0.68で不合格……そこから6回の実験を重ねて、ようやく0.73で合格ラインを突破できました。この記事では、その過程で得た知見を備忘録としてまとめます。
コンペのルールで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ステップで構成しました。
- Step 0: Google Driveのマウント(超重要!)
- Step 1: 依存関係のインストール(numpy, transformers, trl, unsloth等をバージョン固定)
- Step 2: HuggingFaceへのログイン
- Step 3: SFT学習の実行
- 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つの失敗と逆転の一手
