UnityとゲームAIと将棋

Unity、Pythonを中心にゲーム開発やゲームAI開発の技術メモ等、たまに将棋も

【Unity】MLAgentsでターン制ゲームのAIを実装する時の行動決定方法

結論

RequestDecisionメソッドを使用する

詳細

Unity MLAgentsで自作ゲームのAIを実装する際に、サンプルの実装を参考にすると思いますが、サンプルゲームにはリアルタイム性の強いものが多く、ほとんどが下記の画像ようにDecisionRequesterのコンポーネントを利用して毎ステップ行動決定を要求するという実装になっています。

DecisionRequester

そのため、ターン制ゲームでの対戦AIを実装するためにはどうしたらいいのか悩んで調べた内容をメモとして残しておきます。

結論としてはRequestDecisionメソッドを利用します。 ターン管理の仕組みの中で何らかの行動選択をさせたいという部分でRequestDecision()を呼びます。 これによりMLAgents内の行動と行動後の処理が実行されます。以下はサンプルコードです。

        private async UniTask DoMLAgentsAction()
        {
                mlAgent.RequestDecision();
                await UniTask.WaitWhile(() => mlAgent.selectedAction == null);

                // 取得した行動を盤面に反映する何らかの処理

                mlAgent.selectedAction = null;
        }

ここで、MLAgentsによる行動決定をUniTask化して待機している理由はRequestDecisionが呼ばれたタイミングによってはOnActionRecievedが適切に動作せず、インデックスとして0しか返って来なくなることがあるためです。なので、Agentクラスを継承したクラス内にselectedActionという変数を用意して正しく行動が選択されるまで待機するという方法で実装しています。

【Python】Ubuntu で socket.gethostbyname() が 127.0.1.1 を返す時の対処法

結論

/etc/hostsファイル内で以下のように自分のPC名のホスト名が127.0.1.1に指定されているので固定IPに書き換える

127.0.0.1       localhost
127.0.1.1      <自分のPC名>

詳細

Ubuntu 上にて Python で Flask を使ってサーバーを立てるためのコードを書いている際に

host = socket.gethostname()
ADDRESS = socket.gethostbyname(host)

というコードでホストのローカルIPアドレスを取得しようとしていました。 しかし、上記のコードを実行して返ってくるアドレスは

127.0.1.1

で、一体なぜだ?となっていたのですが、色々調べていくうちに/etc/hostsファイル内で以下のようにホスト名が127.0.1.1に指定されていることが判明しました。

127.0.0.1       localhost
127.0.1.1      <自分のPC名> ←これが原因

なぜこうなっているかを調べたところ以下のような記事を発見しました。

qiita.com

どうもDebian系OSの仕様バグっぽいですね。 記事内では

固定IPアドレスが存在する前提で動くシステム/ツール/アプリが不具合を起こすので、回避法として、とりあえず、127.0.1.1を割り当てることが決定、実装されました。

と言及されています。
とりあえず、自分は/etc/hosts内のホスト名を固定IPアドレスに変更して対処しました。

【LLM】大規模言語モデルを動かすのに必要なGPUメモリ

結論

【推論】
推論時の必要GPUメモリ[GB] = パラメータ数[b] × 2 
【学習】
学習時の必要GPUメモリ[GB] = 推論時の必要GPUメモリ[GB] × 4
【n bit 量子化】
量子化時必要GPUメモリ[GB] = 通常時必要GPUメモリ[GB] × ( n / 通常時の bit 数)
【LoRA】
学習時の必要GPUメモリ[GB] = 推論時の必要GPUメモリ[GB]

詳細

GPTのような大規模言語モデル(LLM)を動かすのに必要なGPUメモリを調べたのでまとめておきます。
参考にしたのは以下の二つの資料です。

qiita.com

www.docswell.com

上の二つの資料を総合すると、概算なので正確ではありませんが、以下のようにまとめられます。

【推論】
推論時の必要GPUメモリ[GB] = パラメータ数[b] × 2 
【学習】
学習時の必要GPUメモリ[GB] = 推論時の必要GPUメモリ[GB] × 4
【n bit 量子化】
量子化時必要GPUメモリ[GB] = 通常時必要GPUメモリ[GB] × ( n / 通常時の bit 数)
【LoRA】
学習時の必要GPUメモリ[GB] = 推論時の必要GPUメモリ[GB]

「n bit 量子化
通常は float32 や float16 といった "32 bit" or "16 bit" で計算されている変数を"n bit"の変数として扱って計算する手法です。
「LoRA」
事前学習済みの重みとは別の「ファインチューニング用学習重み」を新しく追加する手法です。

【Python】シェルスクリプトで venv のセットアップをする

結論

シェルスクリプトを source コマンドで叩く必要がある。

python3 -m venv env
source env/bin/activate
pip3 install -r requirements.txt

のようなシェルスクリプトは現在のシェルに反映させるためには

source setup.sh

と叩く。

詳細

仮想環境を venv で管理している Python プロジェクトがあった時に

python3 -m venv env
source env/bin/activate

と毎回打つのは面倒なのでシェルスクリプトでまとめて一括で処理できるようにしようとした時のこと。

python3 -m venv env
source env/bin/activate
pip3 install -r requirements.txt

のようなセットアップ用のシェルスクリプトを作成して

zsh setup.sh

と実行したところ、スクリプト完了後には仮想環境から抜けてしまっていた。
理由を調べていたところ

という違いがあるため現在のシェルで仮想環境に入るには

source setup.sh

と叩く必要があった。

【Node.js】Electronで開発ビルドと本番ビルドを分けたい時の手法

結論

ビルド時に.envファイルに環境変数を書き込む

"scripts": {
    "build:dev": "tsc && touch .env && echo NODE_ENV=development > .env && electron-builder build --mac --x64 --dir",
    "build:prod": "tsc && touch .env && echo NODE_ENV=production > .env && electron-builder build --mac --x64 --dir"
}

解説

Electronで開発をしていると「開発者ツールの表示、非表示を開発環境と本番環境で切り替えたい」などといった、開発環境と本番環境で処理を分けたい時があると思います。 ローカルで試す時は実行時にNODE_ENVを設定してあげれば

// 開発モードかどうか
if(process.env.NODE_ENV === 'development')
{
    // 開発モードなら開発者ツールを開く
    mainWindow.webContents.openDevTools();
}

のように開発環境か本番環境かを区別できるのですが、問題はビルドしてパッケージ化された後にどうするかという部分です。 調べていてよく出てくるのがapp.isPackagedについての情報ですが、これはパッケージ化前後の区別をつけるものなので、ビルド後の開発環境と本番環境を区別したい場合にはあまり有効ではありません。そこで考えたのが、冒頭に書いたようにビルド時に.envファイルに環境変数を書き込む方法です。

"scripts": {
    "build:dev": "tsc && touch .env && echo NODE_ENV=development > .env && electron-builder build --mac --x64 --dir",
    "build:prod": "tsc && touch .env && echo NODE_ENV=production > .env && electron-builder build --mac --x64 --dir"
}

これにより、開発ビルドと本番ビルドで異なるNODE_ENVが使われるため、以下のように dotenv を使うことで区別をつけることができます。

// 環境変数を読み込む
require('dotenv').config({ path: __dirname + 'main.jsからenvファイルまでの相対パス' });

// 開発モードかどうか
if(process.env.NODE_ENV === 'development')
{
    // 開発モードなら開発者ツールを開く
    mainWindow.webContents.openDevTools();
}

もしかしたら electron-builder の build config 等を適切に設定するなどして開発ビルドと本番ビルドを分ける方法があるのかも知れませんが、自分が調べた限りではそういった情報が見当たらなかったので、暫定的に本記事の方法を採用しています。

ゲーム開発でのゲームAI・機械学習技術の活用

概要

ゲーム開発において、ゲームAI技術や機械学習技術を活用する流れが加速していると日々実感しています。実際の現場でも導入されることが増え、ゲーム開発の様々な部分で効率化が進んでいると思います。ただ、どのようなケースで、どのような技術を使用するのが良いのかということについては意外と整理されていないように思います。そこで、自分の経験や調べたことを元に、ゲームAI・機械学習技術の適用ケースを簡単にまとめておこうと思いました。

ゲームAI・機械学習技術の適用ケース

キャラを状況に応じて動かしたい

2D、3D空間上でキャラクターや敵キャラを自動で動かしたいとなった時にまず考えるのがルールベースAIで、コード上でAIの動きをメソッド化するなどして 決めておくという一番シンプルな手法です。
その後、ゲーム状態に応じてAIの挙動を切り替えたいということになった場合には、ゲーム状態を表すenumなどを用意し、ゲーム状態の変化に応じてAIを動かすメソッドを切り替えるというのが有限状態機械(FSM)です。
その他ではニューラルネットワークにキャラの行動を学習させるというのもあるかもしれませんが

  • 学習に使うデータ集め
  • 学習時の誤差関数の設計
  • デバッグの難しさ
  • ゲームデザイナーの意図しない挙動をする可能性

といった課題があるので実装コストに見合っているかは常に考える必要があります。もちろん、AIをより生き物らしく動かしたい場合は、誤差関数の設計に強化学習でいうところの好奇心(内発的報酬)といった概念を導入して学習させてみるといったことも考えられると思います。

対戦用AIを作りたい

対戦用AIは対戦形式がリアルタイムかターン制かによって異なります。リアルタイムの場合はルールベースAIとFSM、深層強化学習あたりがメインになってくると思います。基本的な動きはルールベースで作ってFSMで状況に応じて切り替える、状態と行動の組み合わせが多くてルールベースでは手に負えない or あまりAIが強くならない場合は深層強化学習を使うといった方針でしょうか。深層強化学習では開発時にルールベースAIとの対戦や自己対戦等を行い、対戦で利用する方策(Policy)の学習をし、対戦時にはその方策を利用して人間との対戦を行うという形になります。
一方でターン制の場合はそこにゲーム木探索が入ってきます。ゲーム木探索では学習をしなくて済む+簡単にそこそこ強くできるので、実装コスト的にもまずはゲーム木探索を考えることが多いと思います。もちろんゲーム木探索を行うには探索する際の盤面評価基準が必要なので、そこは人力で調整する必要があります。
また、ゲーム木探索での盤面評価基準を遺伝アルゴリズムで最適化するということもできます。遺伝アルゴリズムにはゲーム開発のシーンでよくある、「AIの強さや戦い方にバリエーションを作りたい」という要請に応えることができるという利点があります。深層強化学習モンテカルロ木探索では弱くなるように(勝率が〇〇%以下になるように等)調整するのが難しいですが、遺伝アルゴリズムであれば適合度関数の目標勝率を低く設定することでゲーム木探索の盤面評価基準のパラメタをAIが弱くなる方向に調整することができます。このように遺伝アルゴリズムで対戦用AIのパラメタ最適化を行う場合は「何に対して勝率を最適化するか?」という「ベースラインモデル」を決める必要があり、ベースラインモデルとしてはルールベースAI、モンテカルロ木探索、深層強化学習といったモデルを利用するという形になることが多いと思います。
加えて、遺伝アルゴリズムと深層強化学習はバランス調整に利用することも可能です。
例えば

  • 遺伝アルゴリズム
    新規キャラクターやカードを追加する際に極端に勝率の高くなるパーティやデッキの組み合わせができてしまわないか(バランスブレイカーの出現)

  • 深層強化学習
    ゲームデザイナーが想定したプレイングとは異なる挙動をする勝率の高いポリシーが獲得されてしまわないかどうか(バグやゲームバランス的な欠陥を突いたプレイングが存在しているかどうか)

といったことを調査するための手法として利用することが考えられます。

動的な難易度調整がしたい

いわゆるメタAIと呼ばれる分野の話です。ルールベースで「N回失敗したら敵を弱くする」などの処理を入れてFSM(ゲーム状態、ゲーム難易度)の切り替えを行うというような形式が基本になるかと思います。ただ、プレイヤーの腕前は「N回失敗した」「N時間でクリアした」などといった要素だけではなく、様々な要素を複合的に考慮しないと正確に判断ができないかと思います。そこで、様々な人のゲームプレイを記録して、ゲームプレイのデータからプレイヤーの腕前を判定するといったような仕組みをニューラルネットによる学習で行うといったことも考えられます。

背景やイラスト作成をしたい

  • Stable Diffusion等の拡散モデル

昔はGANやVAE等のモデルも研究されていたと思うのですが、現在は拡散モデルの1強ですね...著作権に関する議論が煮詰まっていないので、出力された絵をプロダクトに直接使うのにはまだ抵抗や不安があったりする方が多いと思います。なので、現状での活用方法としては自社内でのイメージ伝達で使ったり、自社デザイナー、イラストレーターが作成したラフ画や線画の清書、着色といった作業や既に作成した1枚のキャラ絵からimg2imgでバリエーションを量産するといった作業を自動化するというというのが現実的なラインなのかなと考えています。

ストーリーやセリフを作成したりキャラを喋らせたい

ストーリーやセリフの生成も現在ホットな分野で、GPTモデルをはじめとする各社のLLM(大規模言語モデル)のAPIや基盤モデルを利用するのが主流かと思います。自分たちが作ろうとしているキャラの性格やストーリーの世界観等に沿った形で生成するには、元設定に基づくセリフやストーリーのデータを事前情報として与えるか、それらのデータを用いたファインチューニングを行う必要があります。また、実用上問題になることとしては、倫理的に問題のある発言をしないかどうか、キャラの設定から離れた発言をしてしまう場合にどうするかといったことがあり、これらの点は人力での確認やフィルター処理等を入れる必要が出てきます。

おわりに

業務や個人開発の中でゲームAIや機械学習技術の選定を行う際に考えていたことを簡単にまとめました。実際のゲーム開発プロジェクトにおいては、このように様々なアプローチの仕方を知っておき、特定の手法にこだわらず、状況に応じた技術選定をを行うことがエンジニアとしては重要かなと思います。

【Unity】C#におけるアップキャストとダウンキャスト

Unityでゲームの開発をしている時に派生クラスで追加したフィールドに、取得した基底クラスのインスタンスを使ってアクセスしたいというケースがありました。 このケースを解決するためにアップキャストとダウンキャストについて調べていた際、以下の二つの参考になる記事を見つけました。

teratail.com

qiita.com

自分も最初は上の質問記事のような疑問を持っており、詳しいキャスト変換の仕組みを知りたいと思って調べていた時に見つけたのが下の解説記事です。

この記事中の解説で提示されている図が非常にわかりやすく、参考になりました。

自分の言葉でアレンジして解釈してみると

アップキャスト
→ 派生クラス型の変数を狭い窓(基底クラスの機能)を通して見ているだけで、変数が確保しているメモリ領域が破棄されるわけではない

ダウンキャスト
→ 基底クラス型の変数を広い窓(基底クラス+派生クラスの機能)を通して見るため、未整備のメモリにアクセスしてしまう可能性がある

といったところでしょうか。

つまり、アップキャストを行っても変数に対する見方が変わるだけで、もともと派生クラス型で変数を宣言した際に確保したメモリ領域は残っているため

派生クラス型で変数宣言 → アップキャスト → ダウンキャスト

という処理をしても、派生クラス型変数内のフィールドを参照できるということでした。