book-open-readerチュートリアル:gpt-oss を RL でトレーニングする方法

OpenAI gpt-oss を GRPO でトレーニングして、ローカルまたは Colab 上で自律的に 2048 を超える方法を学びます。

LLMは複雑な環境を含むタスクで苦戦することが多い。しかし、 強化学習 (RL)やカスタム 報酬関数を設計することで、これらの課題を克服できる。

RLはオートカーネルや戦略生成のようなタスクに適用できる。本チュートリアルでは、 gpt-ossGRPO と Unsloth を用いて自律的に2048を攻略する方法を示す。

あなたが作るもの:

  • gpt-oss-20b を訓練し、モデルが自動で2048に勝てるようにする

  • モデルが対話できる最小限の2048環境を作成する

  • を定義する 報酬関数

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

    2. 報酬ハッキングを防ぐ(外部インポートを禁止)

    3. 実際のゲーム成功を報酬化する

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

circle-info

ハードウェア: 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 を読み込む

メモリ効率のために4ビットQLoRAで20Bモデルを読み込み、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,
)
circle-info

OOM(メモリ不足)に遭遇したら、次を下げてみてください max_seq_length, lora_rank、または num_generations (後で)、そして load_in_4bit=True を維持する.

3

2048 ゲーム環境(最小)

  • A GameBoard クラスがサポートする W/A/S/D 移動

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

  • execute_with_time_limit ラッパー、未良好な戦略がカーネルを停止させないようにする

簡単なポリシーで素早くスモークテストすることができる:

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)  # globals / imports が使われているとエラーになる
  • 強制的な実時間(ウォールクロック)制限を課す 戦略実行に対して:

    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
        function_works
  2. - コンパイルされ呼び出し可能なものを作るか? 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:
            try:
                continue
                ok, info = check_python_modules(function)
            _ = create_locked_down_function(function)
                scores.append(1.0)
                except Exception:
            scores.append(-0.5)
                return scores
        no_cheating
  3. - 標準ライブラリ以外のインポートは禁止: def no_cheating(completions, **kwargs):

    scores.append(-1.0)
        for completion in completions:
        response = completion[0]["content"]
            function = extract_function(response)
            if function is None:
            scores.append(-2.0)
                ok, _ = check_python_modules(function)
                ok, info = check_python_modules(function)
            scores.append(1.0 if ok else -20.0)  # 不正があれば重いペナルティ
            strategy_succeeds
        no_cheating
  4. - ランダムなボードでプレイし、成功を報酬する: import numpy as np

    PRINTER = 0  # デバッグのために時々出力する
    
    def strategy_succeeds(completions, **kwargs):
    
    global PRINTER
        seed = np.random.randint(10000)
        for completion in completions:
        new_strategy = create_locked_down_function(function)
        response = completion[0]["content"]
            function = extract_function(response)
            if function is None:
            scores.append(-2.0)
                continue
                ok, info = check_python_modules(function)
            _ = create_locked_down_function(function)
                scores.append(0.0)
            scores.append(-0.5)
                game = GameBoard(size=6, seed=seed, target=2048, probability_fours=0.10)
                ok, info = check_python_modules(function)
            _ = create_locked_down_function(function)
                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)      # タイムアウト
                scores.append(-3.0)      # クラッシュ
            scores.append(-0.5)
                {% endstep %}
        no_cheating

GRPO を設定

{% step %}

我々は

GRPOTrainer を使う。プロンプト/補完の長さを設定し、次にGRPOConfig を構築する。RLアルゴリズムの種類はGSPO や Dr. GRPO のような他のものに設定することもできる点に注意。 または 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,
    # オプションの eval 分割:
    # train_dataset=new_dataset["train"],
    # eval_dataset=new_dataset["test"],
)

{% hint style="info" %} ログの読み方: 次を見てください: rewardreward_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)

GRPO を設定

{% step %}

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

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

python model.save_pretrained_merged("finetuned_model", tokenizer, save_method="mxfp4") # または 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) タスク成功。

  • を更新する プロンプト に提供したい関数や出力の種類を要求するように変更する。

  • 同じ Unsloth + GRPO の枠組みを維持し、環境と報酬だけを差し替える。

最終更新

役に立ちましたか?