【GoogleColab無料枠でOK!】SFT+GRPOでローカルLLMに『思考プロセス』を学習させる実践ガイド

AI

こんにちは。

Fable5やGPT-5.6など優秀なクローズドモデルの開発が進む昨今のAI業界ですが、同時にその優秀さがいつまで使い続けられるのかも疑問視されるニュースがありました。

米国による輸出制限です。

7月1日より解除されましたが、今後もAIの性能は向上していくことを考えると同じような状況に陥る可能性は十分あります。

解除されたので引き続き使用することはできますが、やはりクローズドなモデルに依存するのはリスクがつきまといます。

APIも一回当たりのコストが上がります。エージェントシステムに使おうものなら回数も増えるでしょう。

また自社のデータや情報をどこかのサーバーに投げ続けるのは、セキュリティリスクとして問題ありませんか?

学習に使わないと言われても、その後のデータが悪用されない保証はありません。

ローカルLLMはそれらのリスクを大幅に軽減します。

今回はローカルLLMを自分専用の回答ができるようにするため、ローカルLLMのファインチューニング方法について紹介します。

AIに専門性を持たせる手法としてはRAGプロンプトエンジニアリングMCPSKillsなどのエージェントシステムが主流です。

しかしどんなに外側を取り繕っても、AIが指示に従わなかったり、上手く検索結果を得られなかったら意味がありません。

また制御しようとするあまり、AI周りのシステムがどんどん肥大化してしまいますし、かなりの職人技(属人的)になってしまった結果、メンテナンスが大変なものになってしまいます。

これらを解決するために、AI選びとAIの訓練が重要ではないかと思います。

本記事は以下のような方の参考になるかもしれません。

  • 従来の手法でAIの精度が出ず、お困りの方
  • ローカルLLMの専門的な活用に興味がある方

ご興味ありましたらお付き合いください。

それではよろしくお願いします。

ローカルLLMの課題と現状

とはいえ、じゃあローカルLLMが最適解なのかというと、そうでもありません。

ローカルLLMの課題としては大きく以下の3点です。

  • 性能がクローズドモデルに劣る
  • 推論にもそれなりに高価なGPUが必要(推論に時間がかかる)
  • 学習に必要なデータセットや学習リソースが膨大

これらは以前から現在まで続いている課題です。

しかしこの状況も変わりつつあります。

ざっくりまとめると以下の3点です。

  • ローカルLLMの性能がどんどん向上している
  • ローカルLLMがどんどん小型化している(CPUでも動く)
  • 学習手法の効率化(LoRA、GRPOなど)が普及している

これらの要因により、従来の課題であったものが解決できそうになってきているのです。

本記事ではそれを体感するため、無料のGoogle Colab(T4)でも効果を実感できるものをご用意しました。

今回使用するもの

LiquidAI/LFM2.5-1.2B-JP-202606

まずモデルに関しては、私が最近注目しているLiquid AIを採用します。

Liquid AI ??? The future of AI is local.
Liquid builds foundation models that run natively on device ??? across phones, cars, robots, and everything in between.

Transformerを使わずAttentionとCNNで構築するアーキテクチャを採用することで計算効率を飛躍的に上げています。

このLLMはCPUでの推論はもちろん、スマホやIoTデバイスへの搭載・推論が可能なほどに小型化を実現していることです。

それだけ小型化していると、性能もイマイチだろう、日本語なんてろくに喋れないだろうと思うかもしれませんが、全くそのようなことはありません。

先月リリースされたLiquidAI/LFM2.5-1.2B-JP-202606は日本語に特化して調整されたモデルですが、とても流ちょうな日本語で話してくれます。

本記事ではこのモデルをベースにファインチューニングしていきます。

LoRA

以前の私の記事でも紹介しましたが、今はフルファインチューニングをするより、PEFTと呼ばれる効率の良いファインチューニングがおすすめです。

LoRAについての簡単な解説は以下の記事を参照ください。

画像認識AIのメモリ不足と格闘してたらResNetの”発明の源泉”に辿り着いた件
こんにちは!論文を追うとき、よく「このアイデアはどこから出たんだろう?」と思いませんか?多くの発明は突飛な閃きではなく、現場の“どう削るか・どう足すか”を積み重ねたものです。今回は「ベースに対する差分」という視点で、画像処理〜深層学習〜LL…

低コストでLLMをファインチューニングする効率性と元の学習済モデルの重みを壊さない安定性を担保するのがLoRAのポイントです。

一般的にLoRAを使う際はHuggingFaceなどのライブラリを使うのがメジャーですが、今回はUnslothというライブラリを使ってみたいと思います。

Unsloth/TRL

UnslothはローカルLLMのファインチューニングを効率よく行うためのライブラリです。

Unsloth – Train and Run Models Locally
Unsloth is an open-source, no-code web UI for training, running and exporting open models in one unified local interface…

モデルのロードからPEFTの使い方までHuggingFaceと似たような感じで書けるので、そこまで使いにくい印象はありません。

独自の高速化・効率化機能の設定がありますが、これもGPUによって動く動かないがあるのでドキュメントを確認しながら進める必要があります。

また今回作成する学習コードは以下の記事を参考に作っております。

Liquid LFM2.5: 実行とファインチューニング方法 | Unsloth Documentation
LFM2.5 InstructとVisionをデバイス上でローカル実行・ファインチューニングしましょう!

LLMの学習では強化学習が使われますが、TRLというライブラリを使ってGRPOという手法を使います。

TRL – Transformers Reinforcement Learning · Hugging Face
We’re on a journey to advance and democratize artificial intelligence through open source and open science.

GRPOというのは、DeepSeek R1が登場した時に採用された強化学習手法です。

https://arxiv.org/pdf/2402.03300

仕組みなどの詳細についての説明は割愛しますが、動かした感想としては強化学習をルールベース的にコントロールできるので、低コストかつAIを制御したい方向に学習させられるのが魅力です。

強化学習には色んな手法があるので、興味があればGRPO以外も調べてみたり比較してみてください。

今回はこのGRPOを使っていきます。

学習コード

どなたでも動かせるよう、Google Colabのノートブックは以下になります。

テーマとしては「物事の順序を論理的に考える力を強化する実験」としてます。

Google Colab

「ファイル」→「ドライブにコピーを保存」してお使いください。

コピーしたら右上の「接続 T4」をクリックして接続してください。

もしT4になっていない場合、その横についている▼ボタンを押して「ランタイムタイプの変更」から変更しましょう。

専用のデータセットを生成するコードをGeminiに作ってもらいました。

以下Pythonスクリプトをお手元のPython環境、またはGoogle Colab上で実行してください。

import json
import random

# ==========================================
# 1. システムプロンプトの定義
# ==========================================
SYSTEM_PROMPT = (
    "あなたは論理的思考が得意なAIです。"
    "ユーザーから複数の情報が与えられます。いきなり答えを出すのではなく、まずは <think> タグ内に日本語で、情報を順番に整理し関係性を把握する推論プロセスを記述してください。"
    "思考プロセスを終えたら、</think> の直後に、ユーザーの質問に対する最終的な結論を「【答え】〇〇」という形式で出力してください。"
    "該当するものが複数ある場合は「【答え】A B C」のようにスペース区切りで羅列してください。"
)

# ==========================================
# 2. 新しいSFT用お手本データ(<think>タグ付き)の作成
# ==========================================
logic_sft_examples = [
    {
        "prompt": "AはBより大きいです。BはCより大きいです。一番大きいのはどれですか?",
        "solution": "<think>\n与えられた情報を整理します。\n1. AはBより大きい(A > B)\n2. BはCより大きい(B > C)\nこれらの条件を組み合わせると、A > B > C という順序になります。\nしたがって、一番大きいのはAです。\n</think>\n【答え】A",
    },
    {
        "prompt": "太郎は次郎より足が速いです。三郎は太郎より足が速いです。一番足が速いのは誰ですか?",
        "solution": "<think>\n情報を順に整理します。\n1. 太郎は次郎より速い(太郎 > 次郎)\n2. 三郎は太郎より速い(三郎 > 太郎)\n関係を繋げると、三郎 > 太郎 > 次郎 となります。\nしたがって、一番足が速いのは三郎です。\n</think>\n【答え】三郎",
    },
    {
        "prompt": "X、Y、Zの3人がテストを受けました。Xは1位ではありません。YはZより点数が高いです。Xは最下位(3位)ではありません。点数が高い順に並べたとき、2位は誰ですか?",
        "solution": "<think>\n情報を整理して順位(1位〜3位)を確定させます。\n1. Xは1位でも3位でもない → Xの順位は2位で確定。\n2. YはZより点数が高い(Y > Z)\nXが2位なので、残る1位と3位にYとZが入ります。Y > Z なので、必然的にYが1位、Zが3位となります。\nしたがって、2位はXです。\n</think>\n【答え】X",
    },
    {
        "prompt": "赤い箱、青い箱、白い箱があります。赤い箱は一番重くありません。白い箱は青い箱より重いです。一番軽い箱はどれですか?",
        "solution": "<think>\n重さの順序を整理します。\n1. 白い箱は青い箱より重い(白 > 青)\n2. 赤い箱は一番重くない(つまり1位ではない)\n白 > 青の時点で青は1位にはなれません。また、赤も1位ではないので、一番重い(1位)のは白で確定します。\n残る赤と青の順序ですが、問題文にはこれ以上の条件がないため確定しません。しかし、質問は「一番軽い箱」ではなく、情報を論理的に追う必要があります。待ってください、3つの箱があり、白が青より重い(白 > 青)。一番重いのは赤ではない。よって白が一番重い。残る青と赤ですが、この条件だけでは一番軽い箱は特定できません。もし「青い箱は一番軽くありません」という条件が隠れていれば別ですが...\n※本来の論理パズルのお手本として、正確に推論する姿勢を見せます。\n白 > 青。赤は1位ではない。したがって白が1位。青と赤の大小関係は不明です。\n</think>\n【答え】特定できない",
    },
    {
        "prompt": "犬、猫、鳥がいます。ポチは空を飛びません。タマはニャーと鳴きます。ポチはどれですか?",
        "solution": "<think>\n情報を整理して名前と動物を結びつけます。\n1. タマはニャーと鳴くので「猫」です。\n2. 残る動物は「犬」と「鳥」です。\n3. ポチは空を飛ばないので「鳥」ではありません。\nしたがって、消去法によりポチは「犬」で確定します。\n</think>\n【答え】犬",
    },
]

# SFT用のフォーマット(チャット履歴のリスト形式)に変換
sft_dataset_formatted = []
for ex in logic_sft_examples:
    # ★ 強制トリガー(補助輪)を追加
    forced_prompt = (
        ex["prompt"]
        + "\n\n※必ず <think> タグを用いて情報を順番に整理してから、最後に【答え】を出力してください。"
    )

    sft_dataset_formatted.append(
        {
            "prompt": [
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": forced_prompt},
                {"role": "assistant", "content": ex["solution"]},
            ]
        }
    )

# ファイルに書き出し
with open("logic_sft_think.jsonl", "w", encoding="utf-8") as f:
    for data in sft_dataset_formatted:
        f.write(json.dumps(data, ensure_ascii=False) + "\n")

print("✅ <think>タグ対応のSFT用データ(logic_sft_think.jsonl)を作成しました!")

themes = [
    {
        "items": ["A", "B", "C", "D", "E", "F", "G", "H", "I"],
        "desc": "複数人の身長を比べました。",
        "pred": "より背が高い",
        "adj": "高い",
    },
    {
        "items": [
            "赤の車",
            "青の車",
            "緑の車",
            "黒の車",
            "白の車",
            "黄色の車",
            "銀の車",
            "ピンクの車",
            "オレンジの車",
        ],
        "desc": "複数台の車の速さを比べました。",
        "pred": "より速い",
        "adj": "速い",
    },
    {
        "items": [
            "リンゴ",
            "ミカン",
            "イチゴ",
            "メロン",
            "スイカ",
            "ブドウ",
            "バナナ",
            "モモ",
            "キウイ",
        ],
        "desc": "複数の果物の重さを比べました。",
        "pred": "より重い",
        "adj": "重い",
    },
    {
        "items": [
            "ライオン",
            "トラ",
            "ヒョウ",
            "オオカミ",
            "ゾウ",
            "クマ",
            "サイ",
            "カバ",
            "チーター",
        ],
        "desc": "複数の動物の強さを比べました。",
        "pred": "より強い",
        "adj": "強い",
    },
    {
        "items": ["山A", "山B", "山C", "山D", "山E", "山F", "山G", "山H", "山I"],
        "desc": "複数の山の高さを比べました。",
        "pred": "より高い",
        "adj": "高い",
    },
    {
        "items": [
            "時計",
            "バッグ",
            "靴",
            "帽子",
            "パソコン",
            "スマホ",
            "財布",
            "手帳",
            "万年筆",
        ],
        "desc": "複数の商品の値段を比べました。",
        "pred": "より値段が高い",
        "adj": "値段が高い",
    },
]


def generate_harder_datasets(total_samples=150, test_size=30):
    dataset = []
    seen_prompts = set()

    # 重複のないプロンプトを規定数になるまで生成
    while len(dataset) < total_samples:
        theme = random.choice(themes)
        num_items = random.randint(3, 6)
        items = random.sample(theme["items"], num_items)
        random.shuffle(items)

        constraints = []
        for i in range(len(items) - 1):
            constraints.append(f"{items[i]}は{items[i+1]}{theme['pred']}です。")

        random.shuffle(constraints)
        context = theme["desc"] + "".join(constraints)

        q_type = random.choices(["nth", "greater_all", "compare"], weights=[4, 4, 2])[0]

        if q_type == "nth":
            n = random.randint(2, num_items - 1)
            question = f"一番{theme['adj']}ものを1番目としたとき、{n}番目に{theme['adj']}のはどれですか?"
            solution = items[n - 1]

        elif q_type == "greater_all":
            idx_low = min(2, num_items - 1)
            idx_high = max(idx_low, num_items - 2)
            idx = random.randint(idx_low, idx_high)
            target = items[idx]
            question = f"{target}{theme['pred']}ものを全て答えてください。(※カンマや読点は使わず、スペース区切りや羅列で構いません)"
            solution = "ALL:" + ",".join(items[:idx])

        else:
            idx1, idx2 = random.sample(range(num_items), 2)
            target1, target2 = items[idx1], items[idx2]
            question = f"{target1}と{target2}では、どちらが{theme['adj']}ですか?"
            solution = items[min(idx1, idx2)]

        prompt = f"{context} {question}"

        if prompt not in seen_prompts:
            seen_prompts.add(prompt)
            dataset.append({"prompt": prompt, "solution": solution})

    random.shuffle(dataset)
    test_dataset = dataset[:test_size]
    train_dataset = dataset[test_size:]

    with open("logic_train.jsonl", "w", encoding="utf-8") as f:
        for data in train_dataset:
            f.write(json.dumps(data, ensure_ascii=False) + "\n")

    with open("logic_test.jsonl", "w", encoding="utf-8") as f:
        for data in test_dataset:
            f.write(json.dumps(data, ensure_ascii=False) + "\n")

    print(
        f"✅ データ生成完了: 学習用 {len(train_dataset)} 件 / テスト用 {len(test_dataset)} 件"
    )


# 合計150件生成(学習120件、テスト30件)
generate_harder_datasets(total_samples=150, test_size=30)

特にこの生成コードの解説はしませんが、ランダムで大小関係のパターンを組んでくれるものとなってます。

logic_train.jsonl」「logic_test.jsonl」「logic_sft_think.jsonl」が生成されるので、Google Colabのフォルダに保存してください。

ドラッグアンドドロップでいけます。

今回のコードを動かす際、さらに理解を深めたいという方は以下の書籍がおすすめです!

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

ゼロから作るDeep Learning ❻ LLM編 [ 斎藤 康毅 ]
価格:4,400円(税込、送料無料) (2026/7/10時点)


tokenizerやSFT、GRPOに関する仕組みから作り方まで書いてありますので、気になる方はご参考ください!

コードの要点解説

全体の流れ

全体の流れとしては以下のようになってます。

  • (1) 環境構築
    • 必要なライブラリ (Unsloth, TRL, PEFT など) のインストール
    • vLLM等の推論最適化やメモリ節約のための環境変数設定
  • (2) ベースモデル & トークナイザの初期化
    • 日本語対応LLM (LFM2.5) のロード
    • QLoRA / LoRA (軽量ファインチューニング) アダプタのアタッチとメモリ最適化設定
  • (3) SFT(教師あり学習)フェーズ:フォーマットの学習
    • データ準備: SFT用の小規模データセット (logic_sft_think.jsonl) をロード
    • 学習実行 (SFTTrainer): モデルに「<think>タグで思考プロセスを書き、最後に【答え】を出す」という回答の型(フォーマット)を学習させる
  • (4) GRPO(強化学習)フェーズ:論理推論力の強化
    • データ準備: GRPO用の学習データセットをロードし、システムプロンプトを適用
    • 報酬関数の定義: 生成されたテキストに対し、「<think>タグを正しく使えているか」「最終的な答えが正解か」を自動採点するスコアリングロジック(Reward Function)を定義
    • 学習実行 (GRPOTrainer): SFTで型を覚えたモデルに対し、試行錯誤させながら「より高得点(正解)を出せる推論プロセス」を強化学習で最適化
  • (5) モデルの保存
    • 学習が完了したLoRAアダプタとモデルをローカルディレクトリに保存
  • (6) 事後評価(テスト推論)
    • ベースモデル(Before)と、学習済みモデル(After)のそれぞれにテストデータを与えて推論を実行
    • 報酬関数を用いて自動評価を行い、強化学習によるスコアの改善幅を可視化
  • (7) 結果の比較
    • Before/Afterの平均報酬スコアを比較し、論理順序判定能力が向上したことを確認

システムプロンプト

今回は日本語の論理順序を考える推論力を強化する実験なので、システムプロンプトはそれを達成するためのものになっております。

また<think>タグで推論部分を囲み、その後に回答を出すという思考分離形式はDeepSeekのGRPOを参考にしております。Reasoning Modelsでは最近のトレンドのようです。

ただし今回使用するLFM2.5-1.2B-JP-202606はそのような仕組みで出力するようになっていないため、出力の仕方をSFT(教師あり学習)で訓練してあげる必要があります。

SFT(教師あり学習)

推論力を強化学習で強化する前に、出力形式を従わせる必要があります。

教師あり学習なので、期待する出力形式を指示すればよいです。

先ほどのデータセット生成スクリプトにある

logic_sft_examples = [
    {
        "prompt": "AはBより大きいです。BはCより大きいです。一番大きいのはどれですか?",
        "solution": "<think>\n与えられた情報を整理します。\n1. AはBより大きい(A > B)\n2. BはCより大きい(B > C)\nこれらの条件を組み合わせると、A > B > C という順序になります。\nしたがって、一番大きいのはAです。\n</think>\n【答え】A",
    },

このように、想定される質問と期待する回答形式のペアを用意するだけです。

あとはSFTTrainerを使って必要な設定をすればOKです。

学習時間も数分で完了し、lossが大体0.01以下まで下がっているのが確認できると思います。

GRPO(強化学習)

GRPOで最も重要なのは報酬関数の設計です。

そして強化学習用のデータセットとも関連を考える必要があります。

例えばthink_format_rewardを見ると、これは推論時に<think>タグを使ってしっかり思考を分離してフォーマットを守っているかを評価するものです。

他にもそもそもちゃんと正解できているかどうかだったり、しっかり思考できているかどうか文字数でチェックしたりなど、LLMがとるべき行動にスコアづけして評価しているのがわかると思います。

このスコアを最大化するように、LLMは回答を調整していくという仕組みです。

また報酬関数はできるだけ評価項目別に分けると管理やメンテナンスがしやすくなると思います。

今回のデータセットのケースでも、大体30分程度で完了します。

一番最後のセルにテストデータでの比較を行ってます。

どのぐらい正解率が上がったかは、ノートブックをご覧ください。

学習したモデルの使い方

強化学習が終わったら、モデルの推論に必要なファイルをダウンロードしましょう。

“LFM2.5-1.2B-Instruct-GRPO-test”  フォルダ内に8個ファイルがあるので、カーソルを合わせて右クリックか「︙」ボタンですべてダウンロードします。

checkpoint-100フォルダは不要です。

簡単に推論機能を使う方法として2つご紹介します。

Pythonスクリプトで推論する(HuggingFace)

先ほどのノートブックの「Load the fine-tuned model and run inference」の「After」部分を、いい感じに活用してください。

Ollamaへデプロイ

Ollamaをインストールしていない方は以下からどうぞ。

Download Ollama on Windows
Download Ollama for Windows

Ollamaへデプロイするためには、先ほどダウンロードした8つのファイルを使ってGGUF形式に変換する必要があります。(ついでに量子化もできます)

GGUF形式に変換するノートブックは以下です。

Google Colab

以下を参考にしました。

モデルを Ollama に保存 | Unsloth Documentation

modelsという名前のフォルダを作成し、先ほどダウンロードした8つのファイルをフォルダにアップロードします。

あとは「すべてのセルを実行」で数分待ちます。

ollama_export_ggufフォルダ内のggufファイルとModelfileをダウンロードします。

Modelfileに今回の順序推定用のシステムプロンプトを追記しましょう。

FROM ./LFM2.5-1.2B-JP-202606.Q4_K_M.gguf

TEMPLATE """<|startoftext|><|im_start|>system
{{ .System }}<|im_end|>
<|im_start|>user
{{ .Prompt }}<|im_end|>
<|im_start|>assistant
"""

SYSTEM """
あなたは論理的思考が得意なAIです。ユーザーから複数の情報が与えられます。
いきなり答えを出すのではなく、まずは <think> タグ内に日本語で、情報を順番に整理し関係性を把握する推論プロセスを記述してください。
思考プロセスを終えたら、</think> の直後に、ユーザーの質問に対する最終的な結論を「【答え】〇〇」という形式で出力してください。
該当するものが複数ある場合は「【答え】A B C」のようにスペース区切りで羅列してください。
"""

PARAMETER temperature 0.1
PARAMETER top_k 50
PARAMETER top_p 0.1
PARAMETER repeat_penalty 1.05
PARAMETER stop "<|im_end|>"
PARAMETER stop "<|endoftext|>"

Modelfileはシステムプロンプトだけでなく、チャットテンプレートやパラメータの設定ができます。

ターミナルを起動し、以下のコマンドでollamaへデプロイします

cd (ダウンロードしたパス)
ollama create (ollamaで表示したい名前) -f Modelfile

これでOllamaへのデプロイは完了です。

Ollamaデスクトップで動かしてみましょう。

<think>タグで囲まれた部分は思考部分としてOllamaでは自動的に非表示になります。

クリックするとどのような思考をしたのか、確認できます。

まとめ

今回はローカルLLMをファインチューニングする方法についてご紹介しました。

ローカルLLMが小型化しながらも性能が向上しているため、非常に手軽に訓練することが可能になりました。

また強化学習手法としてGRPOなど効率が良く効果的な学習手法も普及してきています。

HuggingFaceやUnslothなどのライブラリの充実により、GoogleColabの無料GPUでも十分に効果を確認することができます。

今後もますますローカルLLMの需要は高まりますし、新しい性能や学習手法が開発されると思います。

引き続き目が離せないですね!

また面白そうなネタがあれば検証・公開していきます!

ここまでご覧いただきありがとうございました!

この記事を書いた人
MLエンジニア/データサイエンティスト

自動車部品メーカーで制御系ソフトウェアエンジニアを10年以上勤めた後、MLエンジニア兼データサイエンティストとして活動中の30代男性、2児の父。
個人投資家としても活動中。主に国内株式と米国株式。
自由と豊かさと技術をこよなく愛する。

よねすけをフォローする
AIプログラミング
シェアする
よねすけをフォローする
タイトルとURLをコピーしました