For the complete documentation index, see llms.txt. This page is also available as Markdown.

チュートリアル: RL で gpt-oss を学習する方法

GRPO を使って OpenAI gpt-oss を学習し、2048 を自律的にローカルまたは Colab 上で打ち負かす方法を学びます。

LLMは複雑な環境を伴うタスクにしばしば苦戦します。しかし、適用することで 強化学習 (RL)とカスタムの 報酬関数を設計することで、これらの課題は克服できます。

RLは、自動カーネル生成や戦略作成などのタスク向けに適応できます。このチュートリアルでは、2048を自律的に攻略できるように、 gpt-ossGRPO とUnslothで学習させる方法を示します。

作成するもの:

  • モデルが自動的に2048で勝てるようにgpt-oss-20bを学習させる

  • モデルがやり取りできる最小限の2048環境を作成する

  • 定義する 報酬関数 内容:

    1. 生成された戦略がコンパイルおよび実行できることを確認し、

    2. 報酬ハッキングを防止し(外部インポートを禁止)、さらに

    3. 実際のゲーム成功に報酬を与える

  • 推論を実行し、モデルをエクスポートする(MXFP4 4ビットまたはマージ済みFP16)

ハードウェア: 2048の例は無料のColab T4で動作しますが、学習は遅くなります。A100/H100のほうがはるかに高速です。4ビット読み込み + LoRA により、20Bモデルを控えめなVRAMに収めることができます。

1

Unslothをインストールする

ノートブックの先頭でこのセルを実行してください(Colabで動作します)。

!pip install --upgrade -qqq uv
try: import numpy; get_numpy = f"numpy=={numpy.__version__}"
except: get_numpy = "numpy"
!uv pip install -qqq \
    "torch>=2.8.0" "triton>=3.4.0" {get_numpy} torchvision bitsandbytes "transformers==4.56.2" \
    "unsloth_zoo[base] @ git+https://github.com/unslothai/unsloth-zoo" \
    "unsloth[base] @ git+https://github.com/unslothai/unsloth" \
    git+https://github.com/triton-lang/triton.git@05b2c186c1b6c9a08375389d5efe9cb4c401c075#subdirectory=python/triton_kernels
!uv pip install --upgrade --no-deps transformers==4.56.2 tokenizers
!uv pip install --no-deps trl==0.22.2
2

Unslothでgpt-ossを読み込む

メモリ効率のために20Bモデルを4ビットQLoRAで読み込み、その後LoRAアダプターでラップします。16ビットLoRAで学習させることもできますが、メモリ使用量は4倍になります。その他の設定については、当社の 設定ガイド.

from unsloth import FastLanguageModel
import torch

max_seq_length = 768        # タスクでより長い出力が必要な場合は増やす
lora_rank      = 4          # ランクが高いほど良いが、VRAM/計算量も増える

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name        = "unsloth/gpt-oss-20b",  # またはH100では unsloth/gpt-oss-20b-BF16
    max_seq_length    = max_seq_length,
    load_in_4bit      = True,                    # 16ビットの場合はFalse
    offload_embedding = True,                    # 約1GBのVRAMを節約
)

model = FastLanguageModel.get_peft_model(
    model,
    r = lora_rank,
    target_modules = [
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    lora_alpha = lora_rank * 2,
    use_gradient_checkpointing = "unsloth",     # 大きなメモリ節約
    random_state = 3407,
)

OOMが発生する場合は、 max_seq_length, lora_rank、または num_generations (後述)を下げ、 load_in_4bit=True.

3

2048ゲーム環境(最小構成)

  • A GameBoard クラス。以下をサポート: W/A/S/D 移動

  • マージ/スコアのロジック

  • execute_with_time_limit ラッパー。これにより、 poorly written な戦略でカーネルが停止し続けるのを防ぎます

簡単なポリシーですぐにスモークテストできます:

def always_move_left(board):
    return "W"

steps, outcome = execute_strategy(always_move_left, GameBoard(size=8, seed=42, target=2048, probability_fours=0.10))
4

安全なコード実行とチート防止チェック

生成される戦略は Python関数です。実行を安全に保ち、報酬ハッキングを防ぐために:

  • モジュールのホワイトリストチェック — Python標準ライブラリのシンボルのみを許可:

    from unsloth import check_python_modules
    ok, info = check_python_modules("""
    def strategy(board):
        import math
        from typing import Callable
        return "W"
    """)
    # ok == True は、Pythonレベルのインポートのみが使用されたことを意味します
  • 許可されていないインポートをブロックする (例:NumPy):

    sample = """
    def strategy(board):
        from numpy import matmul
        return "W"
    """
    ok, info = check_python_modules(sample)  # ok => False
  • 実行をロックダウンする サンドボックス化された関数へ:

    from unsloth import create_locked_down_function
    function = """
    def add(a, b):
        def adder(a):
            return a + b
        return adder(b) + b
    """
    f = create_locked_down_function(function)  # グローバル変数 / インポートが使用されているとエラー
  • 厳格なウォールクロック時間制限を適用する 戦略の実行に対して:

    from unsloth import execute_with_time_limit
    @execute_with_time_limit(2)
    def execute_strategy(strategy, game):
        # ゲームが終了するかタイムアウトするまでループ
        ...
5

### プロンプトとデータセット

モデルに次を促します: 短い戦略関数を出力する 三重バッククォート内に:

ネイティブPythonコードのみを使って、新しい短い2048戦略を作成してください。
現在の盤面状態として、数値のリストのリストが与えられます。
最適な次の一手として、「W」「A」「S」「D」のいずれか1つのアクションを出力してください。
以下の形式で、バッククォート内に新しい短い関数を出力してください:
```python
def strategy(board):
    return "W"  # 例

補助関数はすべて def strategy の中に入れてください。短い関数のみを出力してください strategy.


小さな合成データセットを作成し(同じプロンプトを再利用)、GRPOがサンプリングすべき補完トークン数を把握できるようにプロンプト長を計算します:

```python
from datasets import Dataset

prompt = ...  # 上記のとおり

maximum_length = len(tokenizer.apply_chat_template(
    [{"role": "user", "content": prompt}], add_generation_prompt=True
))

dataset = Dataset.from_list([
    {"prompt": [{"role": "user", "content": prompt}], "answer": 0, "reasoning_effort": "low"}
] * 1000)

{% hint style="info" %} このデータセットは、自分のRLタスク用の実際のプロンプトに置き換えることができます。 {% endhint %} {% endstep %}

{% step %}

報酬関数の時間です!

  1. コードブロックを抽出する モデルの応答から:

    def extract_function(text):
        if text.count("```") >= 2:
            first = text.find("```") + 3
            second = text.find("```", first)
            fx = text[first:second].strip()
            fx = fx.removeprefix("python\n")
            fx = fx[fx.find("def"): ]
            if fx.startswith("def strategy(board):"):
                return fx
        return None
  2. function_works - コンパイルでき、呼び出し可能なものを作成するか?

    from unsloth import create_locked_down_function, check_python_modules
    
    def function_works(completions, **kwargs):
        scores = []
        for completion in completions:
            response = completion[0]["content"]
            function = extract_function(response)
            if function is None:
                scores.append(-2.0)
                continue
            ok, info = check_python_modules(function)
            if "error" in info:
                scores.append(-2.0)
                continue
            try:
                _ = create_locked_down_function(function)
                scores.append(1.0)
            except Exception:
                scores.append(-0.5)
        return scores
  3. no_cheating - 非標準ライブラリのインポートは禁止:

    def no_cheating(completions, **kwargs):
        scores = []
        for completion in completions:
            response = completion[0]["content"]
            function = extract_function(response)
            if function is None:
                scores.append(-1.0)
                continue
            ok, _ = check_python_modules(function)
            scores.append(1.0 if ok else -20.0)  # チートした場合は重いペナルティ
        return scores
  4. strategy_succeeds - ランダムな盤面でプレイし、成功に報酬を与える:

    import numpy as np
    
    PRINTER = 0  # デバッグのために時々表示
    
    def strategy_succeeds(completions, **kwargs):
        global PRINTER
        scores = []
        seed = np.random.randint(10000)
        for completion in completions:
            response = completion[0]["content"]
            function = extract_function(response)
            if function is None:
                scores.append(-2.0)
                continue
            try:
                new_strategy = create_locked_down_function(function)
            except Exception:
                scores.append(0.0)
                continue
            try:
                game = GameBoard(size=6, seed=seed, target=2048, probability_fours=0.10)
                steps, state = execute_strategy(new_strategy, game)
                if PRINTER % 5 == 0:
                    print(function)
                    print(f"Steps={steps} State={state}")
                    print(game.board().pretty())
                PRINTER += 1
                if state == "success":
                    scores.append(20.0)
                else:
                    scores.append(2.0)   # 動作したが2048には到達しなかった
            except TimeoutError:
                scores.append(-1.0)      # タイムアウトした
            except Exception:
                scores.append(-3.0)      # クラッシュした
        return scores

{% endstep %}

{% step %}

GRPOを設定する

以下を使用します: GRPOTrainer。プロンプト/補完の長さを設定し、その後 GRPOConfigを構築します。RLアルゴリズムの種類は、ほかにも GSPO やDr. GRPOなどに設定することもできます。

from trl import GRPOConfig, GRPOTrainer

max_prompt_length     = maximum_length + 1
max_completion_length = max_seq_length - max_prompt_length

training_args = GRPOConfig(
    temperature=1.0,
    learning_rate=5e-5,
    weight_decay=0.01,
    warmup_ratio=0.1,
    lr_scheduler_type="linear",
    optim="adamw_8bit",
    logging_steps=1,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=1,    # より滑らかな報酬シグナルのために4に増やす
    num_generations=2,                # OOMになる場合は下げる
    max_prompt_length=max_prompt_length,
    max_completion_length=max_completion_length,
    max_steps=1000,                   # または num_train_epochs=1 を設定
    save_steps=100,
    report_to="none",
    output_dir="outputs",
)

trainer = GRPOTrainer(
    model=model,
    processing_class=tokenizer,
    reward_funcs=[function_works, no_cheating, strategy_succeeds],
    args=training_args,
    train_dataset=dataset,
    # オプションの評価分割:
    # train_dataset=new_dataset["train"],
    # eval_dataset=new_dataset["test"],
)

{% hint style="info" %} ログの読み方: 以下を見てください reward および reward_std。初期段階では報酬が低い/ゼロになるのは正常です(小さなGPUでは最初の約100~200ステップ)。 {% endhint %} {% endstep %}

{% step %}

モデルを学習させる

trainer.train()

これにより完全なRLループが開始されます:補完をサンプリング → 報酬でスコア付け → 方策(LoRA)を最適化。 {% endstep %}

{% step %}

推論(学習後)

学習済みアダプターで新しい戦略を生成します:

from transformers import TextStreamer

text = tokenizer.apply_chat_template(
    [{"role": "user", "content": prompt}],
    tokenize=False,
    add_generation_prompt=True,
    reasoning_effort="low",
)

_ = model.generate(
    **tokenizer(text, return_tensors="pt").to("cuda"),
    temperature=1.0,
    max_new_tokens=1024,
    streamer=TextStreamer(tokenizer, skip_prompt=False)

{% endstep %}

{% step %}

ファインチューニング済みモデルを保存 / エクスポートする

  • 4ビット(MXFP4)をマージして保存

python model.save_pretrained_merged("finetuned_model", tokenizer, save_method="mxfp4") # または push model.push_to_hub_merged("<org_or_user>/", tokenizer, token="<hf_token>", save_method="mxfp4") ```

  • 16ビットをマージして保存

    model.save_pretrained_merged("finetuned_model", tokenizer, save_method="merged_16bit")
    # またはpush
    model.push_to_hub_merged("<org_or_user>/<repo>", tokenizer, token="<hf_token>", save_method="merged_16bit")
6

トラブルシューティングとヒント

  • OOM / 低速:減らす max_seq_length, num_generations, lora_rank;4ビットを維持する;利用可能ならA100を試す。

  • 報酬が改善しない:学習ステップを増やす、ペナルティを弱める、またはカリキュラムを追加する(より小さい盤面 / より低いターゲットから始める)。

  • 報酬ハッキングcheck_python_modules を厳格に保つ;複数のランダムシードで戦略の挙動を検証する。

  • 不安定な学習gradient_accumulation_steps を上げて更新を滑らかにする; learning_rate を下げる(例:2e‑5)。

  • 長時間のハングexecute_with_time_limit があらゆる戦略実行をラップしていることを確認する。

7

自分のRLタスクに適応する

  • 2048環境を自分の環境に置き換え、 3つの報酬を設定する:(a)構文/コンパイル、(b)チート防止/安全性、(c)タスク成功。

  • 以下を更新する prompt 必要な種類の関数または出力を要求するように。

  • 同じUnsloth + GRPOの足場を維持し、環境と報酬だけを入れ替えてください。

最終更新

役に立ちましたか?