Decision TransformerによるゲームAI

【概要】 Decision Transformerは強化学習を条件付きシーケンス生成問題として定式化する手法である。目標リターン(将来の累積報酬)を条件として与えることで、その目標を達成する行動系列を生成する。ここでは、5×5グリッドワールドゲームを題材に、AIが目標に応じて異なる戦略を選択する様子を実際に確認できる。


目次

1. はじめに

主要技術:Decision Transformer

論文:Chen, L., Lu, K., Rajeswaran, A., Lee, K., Grover, A., Laskin, M., Abbeel, P., Srinivas, A., & Mordatch, I. (2021). Decision Transformer: Reinforcement Learning via Sequence Modeling. Advances in Neural Information Processing Systems, 34, 15084-15097.

新規性・特徴:強化学習を条件付きシーケンス生成問題として定式化し、目標リターンを条件として行動シーケンスを生成する。目標リターン(returns-to-go)は、現在から将来にかけて得られる報酬の累積値である。Decision Transformerは、この目標値を条件として与えることで、その目標を達成するための行動系列を生成する。従来の強化学習が価値関数を最大化する行動を選択するのとは異なるアプローチである。

使用技術の背景:Transformerは自然言語処理で使われる深層学習モデルで、注意機構(Attention Mechanism)により系列データの長距離依存関係を学習する。GPT-2はTransformerベースの言語モデルで、因果的注意メカニズムにより過去の情報のみを参照して予測を行う。Decision Transformerはこの仕組みを強化学習に応用している。

応用例:ゲームAI、ロボット制御、自動運転の経路計画

体験価値:目標リターンを変更することでAIの行動戦略が変化する様子を観察し、条件付き行動生成の仕組みを理解できる。

Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なPythonライブラリのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)し、以下を実行する。


pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install transformers matplotlib numpy japanize-matplotlib

3. プログラムコード


# Decision Transformerによるグリッドワールドゲームエージェント
# 特徴技術名: Decision Transformer
# 出典: Chen, L., Lu, K., Rajeswaran, A., Lee, K., Grover, A., Laskin, M., Abbeel, P., Srinivas, A., & Mordatch, I. (2021). Decision Transformer: Reinforcement Learning via Sequence Modeling. Advances in Neural Information Processing Systems, 34, 15084-15097.
# 特徴機能: 目標リターン条件付き行動生成 - 目標とする累積報酬(リターン)を条件として与えることで、そのリターンを達成するための行動系列をTransformerで生成する。従来の価値関数ベースやポリシー勾配法とは異なり、強化学習を教師あり学習の枠組みで解決する。
# 学習済みモデル: GPT-2 (openai-community/gpt2) - OpenAIが開発した因果的言語モデル。124Mパラメータ版を使用。Transformerのデコーダーアーキテクチャを採用し、自己回帰的な系列生成が可能。URL: https://huggingface.co/openai-community/gpt2
# 方式設計:
#   - 関連利用技術: PyTorch(深層学習フレームワーク、自動微分とGPU計算)、Hugging Face Transformers(事前学習済みTransformerモデルの利用)、Matplotlib/japanize-matplotlib(結果の可視化と日本語表示)、NumPy(数値計算と配列操作)
#   - 入力と出力: 入力: なし(プログラム内でゲーム環境を生成)、出力: AIエージェントの軌跡可視化とテキスト結果
#   - 処理手順: 1) ランダムプレイによる軌跡データ収集、2) 各軌跡のリターン計算、3) Decision Transformerの学習(状態・行動・リターンの系列から次の行動を予測)、4) 学習済みモデルによる目標リターン条件付きプレイ実行
#   - 前処理、後処理: 前処理: 状態の正規化(グリッド座標を浮動小数点数に変換)、行動のone-hotエンコーディング。後処理: softmax関数による行動確率分布の生成、確率的サンプリングによる行動選択
#   - 追加処理: なし
#   - 調整を必要とする設定値: HIDDEN_DIM(モデルの表現力、デフォルト128)、MAX_SEQ_LENGTH(考慮する履歴長、デフォルト10)、LEARNING_RATE(学習率、デフォルト1e-4)
# 将来方策: HIDDEN_DIMの最適値は、収集した軌跡データの複雑さに基づいて自動調整可能。具体的には、学習損失の収束速度を監視し、収束が遅い場合は次元数を増加させる適応的調整機能を実装できる。
# その他の重要事項: グリッドサイズ5×5、障害物位置は固定、エピソード最大長20ステップ、バッチサイズ100で学習
# 前準備:
#   - pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
#   - pip install transformers matplotlib numpy japanize-matplotlib

import matplotlib.pyplot as plt
import japanize_matplotlib
import torch
import torch.nn as nn
import numpy as np
from transformers import GPT2Model, GPT2Config
import random

# 定数定義
RANDOM_SEED = 42
GRID_SIZE = 5
MAX_EPISODE_STEPS = 20
HIDDEN_DIM = 128  # モデルの表現力を決定(大きいほど複雑なパターンを学習可能だが計算量増加)
MAX_SEQ_LENGTH = 10  # 考慮する過去の履歴長(長いほど長期的な戦略を学習可能)
LEARNING_RATE = 1e-4  # 学習の速度(大きすぎると学習が不安定、小さすぎると収束が遅い)
NUM_EPOCHS = 50
NUM_EPISODES = 500
BATCH_SIZE = 100

# ゲーム環境設定
START_POS = [0, 0]  # 開始位置
GOAL_POS = [4, 4]   # ゴール位置
OBSTACLES = [(2, 2), (3, 1), (1, 3)]  # 障害物位置

# デバイス設定(GPU/CPU自動選択)
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')


# Decision Transformerモデル定義
class DecisionTransformerGameAI(nn.Module):
    def __init__(self, state_dim=2, action_dim=4, hidden_dim=HIDDEN_DIM, max_length=MAX_SEQ_LENGTH):
        super().__init__()

        # GPT-2ベースのTransformerエンコーダ
        config = GPT2Config(
            vocab_size=50257,
            n_positions=max_length * 3,
            n_embd=hidden_dim,
            n_layer=4,
            n_head=4
        )
        self.transformer = GPT2Model(config)

        # 埋め込み層
        self.state_embed = nn.Linear(state_dim, hidden_dim)
        self.action_embed = nn.Linear(action_dim, hidden_dim)
        self.return_embed = nn.Linear(1, hidden_dim)

        # 出力層
        self.action_head = nn.Linear(hidden_dim, action_dim)

        self.max_length = max_length
        self.hidden_dim = hidden_dim

    def forward(self, states, actions, returns_to_go):
        batch_size, seq_len = states.shape[0], states.shape[1]

        # 埋め込み
        state_emb = self.state_embed(states)
        action_emb = self.action_embed(actions)
        return_emb = self.return_embed(returns_to_go.unsqueeze(-1))

        # シーケンスを交互に配置: return、state、actionの順
        sequence = torch.zeros(batch_size, seq_len * 3, self.hidden_dim).to(states.device)
        sequence[:, 0::3] = return_emb
        sequence[:, 1::3] = state_emb
        sequence[:, 2::3] = action_emb

        # Transformer処理
        transformer_out = self.transformer(inputs_embeds=sequence).last_hidden_state

        # 行動予測(action位置のみ)
        action_preds = self.action_head(transformer_out[:, 2::3])

        return action_preds


# グリッドワールドゲーム定義
class GridWorldGame:
    def __init__(self, size=GRID_SIZE):
        self.size = size
        self.reset()

    def reset(self):
        self.agent_pos = START_POS.copy()
        self.goal_pos = GOAL_POS.copy()
        self.obstacles = OBSTACLES
        return np.array(self.agent_pos, dtype=np.float32)

    def step(self, action):
        # 行動: 0=上、1=下、2=左、3=右
        moves = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        new_pos = [
            self.agent_pos[0] + moves[action][0],
            self.agent_pos[1] + moves[action][1]
        ]

        # 境界チェック
        if 0 <= new_pos[0] < self.size and 0 <= new_pos[1] < self.size:
            if tuple(new_pos) not in self.obstacles:
                self.agent_pos = new_pos

        # 報酬計算
        if self.agent_pos == self.goal_pos:
            reward = 10
            done = True
        elif tuple(self.agent_pos) in self.obstacles:
            reward = -5
            done = False
        else:
            reward = -0.1
            done = False

        return np.array(self.agent_pos, dtype=np.float32), reward, done


# メイン処理
print('Decision Transformerによるグリッドワールドゲームエージェント')
print('=' * 60)
print('概要: 強化学習を条件付きシーケンスモデリングとして解くAI')
print('ゲーム: 5×5グリッドで障害物を避けてゴールを目指す')
print('特徴: 目標リターンを指定することで行動戦略を変更可能')
print(f'デバイス: {DEVICE}')
print('=' * 60)

# 乱数シード設定
random.seed(RANDOM_SEED)
np.random.seed(RANDOM_SEED)
torch.manual_seed(RANDOM_SEED)
if torch.cuda.is_available():
    torch.cuda.manual_seed(RANDOM_SEED)
    torch.cuda.manual_seed_all(RANDOM_SEED)

# トレーニングデータ生成
print('トレーニングデータ生成中...')
game = GridWorldGame()
trajs = []

for episode in range(NUM_EPISODES):
    states, actions, rewards = [], [], []
    state = game.reset()

    for step in range(MAX_EPISODE_STEPS):
        action = random.randint(0, 3)
        states.append(state.copy())
        actions.append(action)

        state, reward, done = game.step(action)
        rewards.append(reward)

        if done:
            break

    # returns-to-go計算
    returns_to_go = []
    cumulative = 0
    for r in reversed(rewards):
        cumulative += r
        returns_to_go.append(cumulative)
    returns_to_go.reverse()

    if len(states) > 1:
        trajs.append({
            'states': np.array(states),
            'actions': np.array(actions),
            'returns_to_go': np.array(returns_to_go)
        })

# モデル初期化と学習
print('Decision Transformer学習中...')
model = DecisionTransformerGameAI().to(DEVICE)
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
criterion = nn.CrossEntropyLoss()

for epoch in range(NUM_EPOCHS):
    total_loss = 0
    sample_trajs = random.sample(trajs, min(BATCH_SIZE, len(trajs)))

    for traj in sample_trajs:
        if len(traj['states']) < 2:
            continue

        seq_len = min(MAX_SEQ_LENGTH, len(traj['states']))

        states = torch.FloatTensor(traj['states'][:seq_len]).unsqueeze(0).to(DEVICE)
        actions_one_hot = torch.zeros(1, seq_len, 4).to(DEVICE)
        for i, a in enumerate(traj['actions'][:seq_len]):
            actions_one_hot[0, i, a] = 1.0
        returns_to_go = torch.FloatTensor(traj['returns_to_go'][:seq_len]).unsqueeze(0).to(DEVICE)

        # 予測
        action_preds = model(states, actions_one_hot, returns_to_go)

        # 損失計算
        if seq_len > 1:
            targets = torch.LongTensor(traj['actions'][1:seq_len]).unsqueeze(0).to(DEVICE)
            loss = criterion(action_preds[:, :-1].reshape(-1, 4), targets.reshape(-1))

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            total_loss += loss.item()

    if epoch % 10 == 0:
        print(f'エポック {epoch}、損失: {total_loss/len(sample_trajs):.4f}')

# AIプレイデモンストレーション
print('\nDecision Transformer AIプレイデモ')
game = GridWorldGame()
state = game.reset()

target_return = 8.0
states_hist = [state.copy()]

print(f'目標リターン: {target_return}')
print(f'開始位置: {state}')
print(f'ゴール位置: {game.goal_pos}')
print(f'障害物: {game.obstacles}')

# ゲームプレイ
for step in range(15):
    # 現在までの状態準備
    current_states = torch.FloatTensor(states_hist[-5:]).unsqueeze(0).to(DEVICE)
    dummy_actions = torch.zeros(1, current_states.shape[1], 4).to(DEVICE)
    target_returns = torch.FloatTensor([target_return] * current_states.shape[1]).unsqueeze(0).to(DEVICE)

    # AIが行動を予測
    with torch.no_grad():
        action_preds = model(current_states, dummy_actions, target_returns)
        act_probs = torch.softmax(action_preds[0, -1], dim=-1)
        action = torch.multinomial(act_probs, 1).item()

    # 行動実行
    next_state, reward, done = game.step(action)

    action_names = ['上', '下', '左', '右']
    print(f'ステップ {step+1}: {action_names[action]} → 位置{next_state}(報酬: {reward:.1f})')

    states_hist.append(next_state.copy())
    target_return -= reward

    if done:
        print('ゴール到達')
        break

# 結果可視化
fig, ax = plt.subplots(figsize=(8, 8))

# グリッド描画
for i in range(game.size + 1):
    ax.axhline(i, color='lightgray', linewidth=0.5)
    ax.axvline(i, color='lightgray', linewidth=0.5)

# 障害物
for obs in game.obstacles:
    ax.add_patch(plt.Rectangle((obs[1], obs[0]), 1, 1, color='red', alpha=0.7))

# ゴール
ax.add_patch(plt.Rectangle((game.goal_pos[1], game.goal_pos[0]), 1, 1, color='gold', alpha=0.7))

# エージェントの軌跡
path_x = [s[1] + 0.5 for s in states_hist]
path_y = [s[0] + 0.5 for s in states_hist]

ax.plot(path_x, path_y, 'bo-', markersize=8, linewidth=2, alpha=0.7, label='AI軌跡')
ax.plot(path_x[0], path_y[0], 'go', markersize=12, label='開始')
ax.plot(path_x[-1], path_y[-1], 'ro', markersize=12, label='終了')

ax.set_xlim(0, game.size)
ax.set_ylim(0, game.size)
ax.set_aspect('equal')
ax.invert_yaxis()
ax.set_title('Decision Transformer AIプレイ結果', fontsize=14, pad=20)
ax.legend()

# 凡例説明
ax.text(0.5, -0.7, 'AI軌跡(青線)、障害物(赤)、ゴール(黄)',
        transform=ax.transData, fontsize=10, ha='left')

plt.tight_layout()
plt.show()

# 結果出力
print(f'\n総移動ステップ数: {len(states_hist)-1}ステップ')
print(f"ゴール到達: {'はい' if np.array_equal(states_hist[-1], game.goal_pos) else 'いいえ'}")

# AIが学習した戦略データ
with torch.no_grad():
    # 異なる目標リターンでの戦略比較
    high_target = torch.FloatTensor([[8.0]]).unsqueeze(0).to(DEVICE)
    low_target = torch.FloatTensor([[2.0]]).unsqueeze(0).to(DEVICE)
    current_state = torch.FloatTensor(states_hist[-1:]).unsqueeze(0).to(DEVICE)
    dummy_action = torch.zeros(1, 1, 4).to(DEVICE)

    high_probs = torch.softmax(model(current_state, dummy_action, high_target)[0, -1], dim=-1).cpu()
    low_probs = torch.softmax(model(current_state, dummy_action, low_target)[0, -1], dim=-1).cpu()

print(f'\n目標別戦略(上下左右の確率): 高目標[{high_probs[0]:.2f}、{high_probs[1]:.2f}、{high_probs[2]:.2f}、{high_probs[3]:.2f}] 低目標[{low_probs[0]:.2f}、{low_probs[1]:.2f}、{low_probs[2]:.2f}、{low_probs[3]:.2f}]')
print(f'シーケンス学習結果: 履歴{len(states_hist)}ステップ→決定確信度{act_probs.max():.3f} ゴール効率{int(abs(states_hist[-1][0]-4)+abs(states_hist[-1][1]-4))}マス差')
print('読み方: 高目標時と低目標時で行動確率が変化=目標条件付き戦略、確信度=過去経験からの学習度、マス差=最適解からの距離')

print('\nDecision Transformerの特徴:')
print('・目標リターンを条件として与えることで行動を生成')
print('・従来の強化学習と異なるTransformerベースのアプローチ')
print('・シーケンス生成により長期的な戦略を学習')

# 結果をresult.txtに保存
with open('result.txt', 'w', encoding='utf-8') as f:
    f.write('Decision Transformer実行結果\n')
    f.write(f'総移動ステップ数: {len(states_hist)-1}ステップ\n')
    f.write(f"ゴール到達: {'はい' if np.array_equal(states_hist[-1], game.goal_pos) else 'いいえ'}\n")
    f.write(f'最終位置: {states_hist[-1]}\n')
    f.write(f'目標別戦略(高目標): 上{high_probs[0]:.2f}、下{high_probs[1]:.2f}、左{high_probs[2]:.2f}、右{high_probs[3]:.2f}\n')
    f.write(f'目標別戦略(低目標): 上{low_probs[0]:.2f}、下{low_probs[1]:.2f}、左{low_probs[2]:.2f}、右{low_probs[3]:.2f}\n')
    f.write('目標別戦略分析結果を保存しました\n')

print('result.txtに保存しました')

4. 使用方法と結果の見方

実行手順

  1. 上記のプログラムを実行する
  2. 実行すると、AIの学習過程が表示され、学習完了後にグリッドワールドでのAIプレイデモが実行される。最後に結果のグラフが表示される。

結果の解釈指針

決定確信度が0.7以上の場合は学習が進んでいることを示す。目標別戦略で高目標と低目標の行動確率分布が異なる場合、AIが目標に応じて戦略を変更していることを意味する。ゴール効率が0に近いほど最短経路に近い。

トラブルシューティング

損失が減少しない場合:学習率を調整(0.0001から0.01の範囲で試行)、エポック数を増加。AIがゴールに到達できない場合:目標リターン値を調整(5.0から12.0の範囲で試行)、学習データ量(NUM_EPISODES)を増加。

5. 実験・探求のアイデア

AIモデル選択

GPT2Config内のパラメータ変更により異なるモデル構成を試すことができる。n_layer(層数)やn_head(アテンションヘッド数)を変更して性能を比較する。

実験要素

  1. 目標リターン値の変更:target_returnを1.0から15.0まで変化させ、AIの行動戦略がどのように変わるか観察する。
  2. グリッドサイズの変更:GRID_SIZE定数を7や10に変更し、より大きな環境でのAIの振る舞いを確認する。
  3. 障害物配置の変更:obstaclesリストを編集して異なる迷路パターンを作成し、AIの適応能力を検証する。
  4. 学習エポック数の調整:NUM_EPOCHSを25、100、200と変更し、学習量による性能変化を測定する。
  5. 履歴長の影響:states_history[-5:]の数値を3や10に変更し、過去情報量がAIの判断に与える影響を調査する。

体験・実験・探求のアイデア(新発見を促す)

  1. 報酬設定(ゴール報酬、障害物ペナルティ、移動コスト)を変更し、AIの行動優先順位がどう変わるか観察する。
  2. 同じ目標リターンでも実行ごとに異なる経路を取る場合があることに注目し、確率的な行動選択の意味を考察する。