「AIにゲームキャラを自分で学習させたい」——そう思ってUnityのML-Agentsを試してみました。想像以上に深くて、環境構築だけで半日かかりましたが、エージェントが少しずつ「賢く」なっていく様子は本当に面白かったです。今回はその一部始終をご紹介します。

強化学習とML-Agentsって何だろう?

まず基本から。強化学習(Reinforcement Learning)とは、AIが「試行錯誤」を繰り返しながら自分で最適な行動を学ぶ仕組みです。

  • 観測(Observation):AIが「見える」情報(自分の位置、目標の位置、速度など)
  • 行動(Action):AIが取る動作(どっちに力を加えるか)
  • 報酬(Reward):行動の良し悪しを数値で教える(ゴールに到達したら+1、落ちたら−1)

人間が「正解のやり方」を教えるのではなく、報酬という「評価」を与えることで、AIが自分で最適解を探し出すのが強化学習の肝です。

Unity ML-AgentsはUnity向けの強化学習フレームワークで、UnityのゲームエンジンをAIの「学習環境」として使えます。Unityがシミュレーション環境を担当し、PythonのAIライブラリ(PyTorch)が学習アルゴリズムを担当、両者がgRPCで通信する仕組みです。

ML-Agents アーキテクチャ図

環境セットアップが意外とハードだった

ML-Agentsのセットアップは、一筋縄ではいきませんでした。特にmacOS(Apple Silicon)での依存関係の解決に苦労しました。

前提:バージョン制約が厳しい

mlagents 1.1.0(最新版)を動かすにはPython 3.10.12以下が必要です。標準的なmacOSに入っているPython 3.12や3.11では動きません。pyenvでPython 3.10.12を別途インストールする必要があります。

# pyenvでPython 3.10.12をインストール
CFLAGS="-I$(brew --prefix xz)/include" \
LDFLAGS="-L$(brew --prefix xz)/lib" \
pyenv install 3.10.12

# 仮想環境を作成
python3.10 -m venv ~/devel/unity/ml_agents_env
source ~/devel/unity/ml_agents_env/bin/activate

ハマりポイント①:grpcioのビルドエラー

mlagentsをそのままpip installすると、grpcioのソースビルドが始まり、pkg_resourcesが見つからないとエラーになります。解決策は--prefer-binaryフラグです。

# grpcioはバイナリホイールを強制指定
pip install "grpcio>=1.11.0,<1.49.0" --prefer-binary

ハマりポイント②:protobufバージョン競合

mlagentsはprotobuf 3.x系が必要ですが、デフォルトでは最新版(7.x)が入ってしまいます。

pip install "protobuf<3.21" --prefer-binary

ハマりポイント③:mlagents自体は--no-depsで

mlagentsの依存関係解決が荒れるため、本体は--no-depsでインストールし、依存パッケージを個別に入れるのが最も安定します。

pip install mlagents==1.1.0 --no-deps
pip install mlagents-envs==1.1.0 --no-deps --prefer-binary
pip install torch torchvision cloudpickle gym --prefer-binary

Unityパッケージ側の注意点

Unity 6(6000.0系)を使う場合、com.unity.ml-agents4.0.3以降が必要です。manifest.jsonに直接記述します。

{
  "dependencies": {
    "com.unity.ml-agents": "4.0.3",
    ...
  }
}

RollerBallシーンを作る

今回作ったのはRollerBall——白い球(エージェント)が赤い球(ターゲット)を目指して転がるシンプルなシーンです。強化学習の教科書的なサンプルです。

シーン構成

  • Floor:Plane(Scale 10, 1, 10)——落ちると失敗の床
  • Target:球体(赤)——エピソードごとにランダム位置に配置
  • RollerAgent:球体(白)——学習するエージェント本体

RollerAgentには以下のコンポーネントを追加します:

  • Rigidbody——物理演算用
  • RollerAgent.cs——独自スクリプト(後述)
  • Behavior Parameters——ML-Agentsの設定(観測数・行動数など)
  • Decision Requester——何ステップごとにAIに判断させるか

Behavior Parametersの設定値:

  • Behavior Name: RollerBall
  • Vector Observation Space Size: 8
  • Continuous Actions: 2

エージェントのC#スクリプト解説

AIの「目」「手」「評価」を定義するのがこのスクリプトです。Agentクラスを継承して4つのメソッドをオーバーライドします。

using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Actuators;

public class RollerAgent : Agent
{
    public Transform target;
    public float speed = 10f;
    private Rigidbody rb;

    // 初期化:Rigidbodyを取得
    public override void Initialize()
    {
        rb = GetComponent<Rigidbody>();
    }

    // エピソード開始時:Targetをランダム位置に、自分は中央に戻す
    public override void OnEpisodeBegin()
    {
        if (transform.localPosition.y < 0)
        {
            rb.angularVelocity = Vector3.zero;
            rb.linearVelocity = Vector3.zero;
            transform.localPosition = new Vector3(0, 0.5f, 0);
        }
        target.localPosition = new Vector3(
            Random.value * 8 - 4, 0.5f, Random.value * 8 - 4);
    }

    // 観測収集:AIが「見る」情報を8次元で渡す
    public override void CollectObservations(VectorSensor sensor)
    {
        sensor.AddObservation(target.localPosition);   // 3次元
        sensor.AddObservation(transform.localPosition); // 3次元
        sensor.AddObservation(rb.linearVelocity.x);    // 1次元
        sensor.AddObservation(rb.linearVelocity.z);    // 1次元
    }

    // 行動受け取り&報酬付与:AIの判断を受けて物理演算、評価する
    public override void OnActionReceived(ActionBuffers actions)
    {
        Vector3 controlSignal = Vector3.zero;
        controlSignal.x = actions.ContinuousActions[0];
        controlSignal.z = actions.ContinuousActions[1];
        rb.AddForce(controlSignal * speed);

        float distToTarget = Vector3.Distance(
            transform.localPosition, target.localPosition);

        if (distToTarget < 1.42f)
        {
            SetReward(1.0f);   // ゴール到達! +1
            EndEpisode();
        }
        if (transform.localPosition.y < 0)
        {
            SetReward(-1.0f);  // 転落… -1
            EndEpisode();
        }
    }
}

ポイントは報酬設計です。この例では「ゴールに着いたら+1、落ちたら−1」というシンプルな報酬のみですが、これだけでAIは「転ばずにゴールを目指す」という戦略を自力で発見します。

PPO設定ファイルの中身

Pythonの学習アルゴリズムの設定は.yamlファイルで行います。今回使ったのはPPO(Proximal Policy Optimization)というアルゴリズムで、現在の強化学習では最もよく使われる手法のひとつです。

behaviors:
  RollerBall:
    trainer_type: ppo
    hyperparameters:
      batch_size: 10
      buffer_size: 100
      learning_rate: 3.0e-4
      beta: 5.0e-4
      epsilon: 0.2
      lambd: 0.99
      num_epoch: 3
      learning_rate_schedule: linear
    network_settings:
      normalize: false
      hidden_units: 128
      num_layers: 2
    reward_signals:
      extrinsic:
        gamma: 0.99
        strength: 1.0
    max_steps: 500000
    time_horizon: 64
    summary_freq: 10000

主なパラメータの意味:

  • batch_size / buffer_size:一度に学習するデータ量。小さいほど更新が頻繁になる
  • learning_rate:学習率。大きすぎると発散、小さすぎると収束が遅い
  • hidden_units / num_layers:ニューラルネットの規模(128ユニット×2層)
  • gamma:将来の報酬をどれだけ重視するか(0.99 = かなり先まで考慮)
  • max_steps:最大学習ステップ数(500,000ステップで学習終了)

学習の実行コマンド

source ~/devel/unity/ml_agents_env/bin/activate
cd ~/devel/unity/MLAgents_RollerBall

mlagents-learn config/rollerball.yaml --run-id=RollerBall_01

このコマンドを実行するとPython側が待機状態になり、UnityエディタでPlayボタンを押すと通信が確立して学習がスタートします。コンソールにRegistered Communicator in Agent.と表示されれば成功です。

実際に学習させてみた結果

実際にUnity EditorをPlay Modeにして学習を開始しました。Pythonターミナルには10,000ステップごとにログが流れ、Unityの画面ではボールが動き回るのが見えます。

学習が進むにつれて、エージェントの挙動が変化していくのが興味深かったです。最初はランダムに動き回り、たまたまゴールに当たる→転落する、を繰り返していました。

学習ログ・報酬とエピソード長の推移

学習ログから読み取れること

TensorBoard(学習の可視化ツール)のログをtbparseライブラリで読み込んで分析した実測データです。

観察された挙動の変化

ステップ数 平均報酬 エピソード長 エージェントの様子
10,000 +0.017 37ステップ すぐ転落。たまにゴール到達
20,000 0.0 411ステップ 転落は減った。ゴールは見つけられず
40,000〜50,000 0.0 13,000〜14,000ステップ 転ばない!でも止まってる?

「賢くなりすぎた」落とし穴

面白い現象が起きました。エピソード長が急増(37 → 14,000ステップ)していることから、エージェントは「転ばない」という負の報酬(−1)を避けることを先に学習してしまったようです。

床の端に近づかないようにゆっくり動く——これで「転落ペナルティ」は回避できます。しかし「ゴール到達ボーナス(+1)」を取りにいかないため、エピソードが終わらず長くなっています。

これは強化学習の典型的な課題のひとつ、「報酬ハッキング(Reward Hacking)」の一形態です。「悪いことをしない」ことを学んでも、「良いことをする」とは限らない。

💡 改善のヒント
この問題には「ステップごとに小さなペナルティを与える」設計が有効です。
例:AddReward(-0.001f) を毎ステップ加算することで「早くゴールを目指す」動機が生まれます。

より長く学習させると…

今回は50,000ステップで途中経過を見ましたが、一般的にこのタスクでは200,000〜300,000ステップあたりで学習が収束し始めます。典型的な収束後の挙動は:

  • 平均報酬が0.7〜0.9まで上昇
  • エピソード長が20〜30ステップ程度まで短縮
  • エージェントがほぼ一直線にターゲットへ向かう

TensorBoardで可視化する

source ~/devel/unity/ml_agents_env/bin/activate
tensorboard --logdir ~/devel/unity/MLAgents_RollerBall/results/

# ブラウザで http://localhost:6006 を開く

TensorBoardを使うと、報酬の推移・Policy Loss(方策の変化量)・Value Loss(価値関数の誤差)などをリアルタイムで可視化できます。学習が順調かどうかをひと目で確認できるので必須のツールです。

NavMeshとの使い分け

以前の記事(p910: UnityのNavMeshでAIキャラに自動経路探索させてみた)でNavMeshを紹介しましたが、ML-Agentsと何が違うのでしょうか?

観点 NavMesh ML-Agents
動作の仕組み 事前に計算した最短経路を辿る 試行錯誤で自力学習
セットアップ 数分〜数時間(ベイク) 数時間〜数日(学習)
予測可能性 高い(同じ状況なら同じ行動) 低い(確率的な行動)
適した場面 RPGのNPC移動・ゲームAI 複雑な物理・未知の環境・研究
動的な障害物 NavMesh Obstacleで対応 自然に対応(観測に含めれば)

一般的なゲーム開発ならNavMesh物理ベースの複雑な行動や研究目的ならML-Agentsというのが現実的な使い分けです。

まとめ

Unity ML-Agentsで強化学習エージェントを育ててみた体験をまとめました。

  • 環境構築:Python 3.10.12 + venv + --no-deps + --prefer-binary で依存地獄を突破
  • シーン構成:Floor + Target + RollerAgent(Rigidbody + Behavior Parameters + Decision Requester)
  • 学習の仕組み:観測8次元 → PPOニューラルネット → 行動2次元 → 報酬でフィードバック
  • ⚠️ 落とし穴:報酬ハッキング——「転ばない」を先に学んで「ゴールを目指す」を後回しにした
  • 📖 次のステップ:ステップペナルティの追加、複数エージェントの並列学習、カリキュラム学習

強化学習の「AIが自分で賢くなる」面白さを体感できただけで大満足です。完全収束した姿を見るには、もう少し学習を続ける必要がありますが、その過程——ランダムに動く → 転ばないことを学ぶ → ゴールを目指し始める——というプロセスこそが強化学習の醍醐味だと感じました。

それでは、今回はここまで。ありがとうございました😊