![images](../img/imgtop_sub_y.png)
独自のやり方で学習させたニューラルネットのモデルを、ゲームエンジン側で動かします。
![images](../img/imgcmm_202304151058126226295650.jpg)
関連
- ◯
- ML-Agents の低レベルAPIを使う(トレーニング編):LLAPI, Python, Unity
- ◯
- コンテナに強化学習のフレームワークを設置する:Docker, ML-Agents
概要
せっかく独自のやり方で強化学習を行ったわけですから、学習させたニューラルネットのモデル(NNモデル)もゲームエンジンで使いたいですよねーー
次は、ML-Agents の低レベルAPI(LLAPI )を使って学習させたNNモデルを、ゲームエンジン側(Unity )で動かすやり方です。
実装
次のように実装します:[※1]
- ・
- Python 側 …… 学習の最後に、パラメータを更新したNNモデルを、標準形式で保存するコードを追加します。
- ・
- Unity (C#)側 …… 標準形式のNNモデルを使って、エージェントを動かします。
- ※1
- ML-Agents のフレームワークから得たNNモデルなら、訓練に使った3Dモデルにそのまま取り付ければ動きますーーしかし独自のコードで学習させたものは、そういう工夫がされているわけではありませんーーなので訓練とはべつに、NNモデルに観察を与え〜行動の指針を得るスクリプトを書く必要があります。
実装:Python
Python側のスクリプトを記述します。
エージェントのクラスに、NNモデルの実体(インスタンス)を得るためのメソッドを追加します:
class Agent: ... def getnnm(self): return self.pi
あとは訓練部分に、NNモデルを標準形式で保存する記述を加えるだけです:[※1]
# トレーニングの冒頭に追加: pthnnx = '<nn_model>.onnx' # NNモデルの保存場所 cntsve = int(trials) - 1 # エピソードの最後に追加: if cntepi == cntsve: nnmtgt = agtnnm.getnnm() vernnx = 9 # オプセットのバージョンはこれが推奨されていますが、これ以上でも動きます namset = 'sensor' namget = 'action' dmyset = torch.zeros(4).unsqueeze(0) torch.onnx.export( nnmtgt, dmyset, pthnnx, opset_version=vernnx, export_params=True, do_constant_folding=True, input_names=[namset], output_names=[namget] )
- ※1
- Pytorch のonnx.export メソッドを使うことで、標準形式(ONNX)で保存できます。
実装:Unity
Unity 側のスクリプトを記述します。
必要なモジュール群を読み込み:[※1]
using UnityEngine; using Unity.Barracuda;
- ※1
- Barracuda は、Unity で標準のNNモデル(ONNX)をあつかうライブラリです。
名前空間とクラスを定義し、クラス内で使う変数群を定義〜初期化します:
namespace <namespace_project> { public class <monobehaviour_class>: MonoBehaviour { [SerializeField] public NNModel nnmtgt; // この項目からNNモデルを取得します public int prddcs = 5; public float fcemov = 100.0f; private int cntstp; private int flgact = 0; private GameObject objtgt, objpol; private Rigidbody rgdtgt, rgdpol; private Model nnmrun; private IWorker nnmwrk; private float[] lstget = new float[] {0, 0}; // ... 各メソッドの定義 } }
以降、それぞれのメソッドの定義ですーーゲーム開始時の初期設定を行います:
void Start() { objtgt = this.gameObject; rgdtgt = objtgt.GetComponent<Rigidbody>(); objpol = objtgt.transform.Find("<name_pole>").gameObject; rgdpol = objpol.GetComponent<Rigidbody>(); nnmrun = ModelLoader.Load(nnmtgt); nnmwrk = WorkerFactory.CreateWorker(WorkerFactory.Type.CSharp,nnmrun); // ワーカはもっとも無難なタイプを使っていますが、動きそうならより効率的なタイプを試せます cntstp = 0; }
ゲームのフレームごとの処理を記述しますーーML-Agents は、強化学習の環境を再現させるよう、フレームの更新は(Updateではなく)時間と連動するFixedUpdate を使っています。なので再生には、こちらのメソッドを使います:[※2]
void FixedUpdate() { float postgt, vlotgt, angpol, vlopol; float[] lstset; Tensor tenset; // (n:1,h:1,w:1,c:4) Tensor tenget; // (n:1,h:1,w:1,c:2) // ... 観測の記述 // ... 行動の記述 }
- ※2
- 参照:
- ・
- https://docs.unity3d.com/Packages/com.unity.ml-agents@1.0/api/Unity.MLAgents.Academy.html
- ・
- https://genkitech.net/unity-ml-agents-step-frame
環境からの観測をNNモデルに与え、行動の指針を得ますーーBarracudaは、NNモデルを画像認識を前提にした形式であつかっているので、その形式に沿って値を与えます(ここでは、最後のチャネル次元に観測結果の配列を与えます)。なお学習時のフレームごとの観測取得タイミング(DecisionRequester > decisionPeriod )と合わせるために、指定のフレーム数だけスキップします:[※3]
cntstp = cntstp + 1; if (cntstp >= prddcs) { postgt = objtgt.transform.position.x; vlotgt = rgdtgt.velocity.x; angpol = SymmetricalRadian(objpol.transform.localEulerAngles.z * Mathf.Deg2Rad); vlopol = rgdpol.angularVelocity.z; lstset = new float[] {postgt,vlotgt,angpol,vlopol}; tenset = new Tensor(1,1,1,4,lstset); nnmwrk.Execute(tenset); tenget = nnmwrk.PeekOutput(); lstget = new float[] {tenget[0,0,0,0],tenget[0,0,0,1]}; cntstp = 0; }
- ※3
- NNモデルにはExecute() で入力を与え、PeekOutput()から出力を得ます。
NNモデルが返した行動の指針(ここでは行動のそれぞれの確率)から、エージェントに行動を与えますーーここでは、確率の大きい方の行動を選択するようにしています:
if (lstget[0] >= lstget[1]) { rgdtgt.AddForce(new Vector3(+ fcemov,0.0f,0.0f)); } else { rgdtgt.AddForce(new Vector3(- fcemov,0.0f,0.0f)); }
次は、観察で得たポールの傾き(ラジアンの値)を、正負で対称にするための変換です:
private float SymmetricalRadian(float radorg) { if (radorg > Mathf.PI) { radorg = radorg - Mathf.PI * 2; } return radorg; }
再生
Unity のエディタからプロジェクトを実行すれば、取り付けたNNモデルでエージェントが動きます:[※1]
- ※1
- あまり訓練に時間をかけてないので、学習できてないですね……