咱們此次來學習一個新的實例——Push Block。這個示例的效果以下:html
如圖能夠看到,這個示例訓練的效果就是讓agent把白色的方塊推到場景中綠色條形區域就算完成任務。注意每完成一次任務,重置的時候地面會顯示綠色,代表這次agent成功將白色方塊推到目標區域,若失敗,則地面會顯示一下紅色。c#
咱們能夠先根據以前示例,想一下這個agent應該如何進行訓練。首先可能想到的應該是利用Viusal Observations來對這個功能進行訓練,其實本質上這個示例和Grid World有點類似,利用Render Texture Sensor
或者Camera Sensor
來對其進行訓練。實際上官方在Push Block示例中,也有個場景叫VisualPushBlock
,這個場景就是想嘗試使用Camera Sensor
來對agent進行訓練,你還會發現它使用的是第一人稱攝像機,固然官方並無把這個訓練完成(即沒有訓練模型)。緩存
這個示例主要咱們能夠學習到的就是如何利用射線傳感器進行數據採集,即Ray Perception Sensor
。這個傳感器主要分爲2D和3D兩個類別,分別適用於二維和三維。Push Block則採用了Ray Perception Sensor 3D
類型。dom
下面老規矩,先來看看官方對該示例的參數。ide
設定:在一個平臺環境中,agent能夠推進方塊移動。函數
目標:Agent必須推進方塊到目標位置。學習
Agents:一個訓練環境只包含一個agent。this
Agent獎勵設定:編碼
行爲參數spa
矢量觀測空間:(Continusous)70個觀測變量分別對應14個射線投射(ray-casts),每條射線探測三個可能的目標(牆、目標區域或方塊)之一。即一條射線要麼啥都沒探測到,要麼探測到的目標爲牆、目標區域或方塊三者之一。注意這裏的目標區域其實也是有一個Box Collider使得射線能夠檢測到。
這裏有個疑問,70個觀測變量怎麼來的= =。有知道的小夥伴能夠留言討論一下~(在寫後面的時候忽然發現怎麼算出來的了,具體在3、場景基本結構中解釋Ray Perception Sensor Component 3D
組件部分)
矢量動做空間:Discrete Type
,6個變量,分別對應小agent順時針、逆時針方向旋轉以及前、後、左、右四哥方向的移動。
視覺觀察(可選):使用第一人稱攝像機,示例在VisualPushBlock
場景中。可是官方文檔也說了,Push Block的視覺觀察版本並不使用提供的默認訓練參數進行訓練,也就是說若是想利用視覺觀測值進行訓練,須要本身調試一下訓練參數。
可變參數:4個
以上參數中後三個:dynamic_friction、static_friction和block_drag其實都是調整agent小藍塊推進白色方塊的難易程度,摩擦係數或者空氣阻力越大,那麼agent就越難推進白色方塊,或者推進得越慢。
基準平均獎勵:4.5
場景中包含了32個訓練單元,以下圖:
咱們先從上到下介紹一下簡單的東西:SingleCam
是場景渲染相機,即運行時觀察的場景;PuashBlockSettings
是設置環境一些高級物理設置以及專對PushBlock的設置,例如agent的速度,旋轉速度等等;剩下就是UI、光源等,不必詳細介紹。
主要來看一下基本訓練單元Area
的結構,以下圖:
其中:
Agent
是咱們要訓練的代理,藍色的小方塊。它身上不只有Push Agent Basic
訓練腳本,還有兩個Ray Perception Sensor Component 3D
組件。咱們能夠先來看一下射線組件是怎麼樣的,以下圖:
首先咱們的agent身上是有兩個Ray Perception Sensor的,分爲上層和下層,每一層上又有7條射線投射出。爲何分爲兩層呢?咱們來看一下圖就明白了:
這裏先忽略圖中的字樣,有繪製空心球的地方就是射線檢測碰撞到的地方。若是小藍只有底下一層Ray Sensor的話,當小藍推進小白時,小藍的前方射線勢必會被小白遮擋幾條,這就至關於小藍被帶了眼罩看不清前方。所以爲了防止這種狀況,咱們給小藍的上方又加了一層ray sensor,這一層射線是永遠不會被小白遮擋的,因此小藍纔有了兩層Ray Sensor的緣由。
除此以外,我發現官方作的Ray Sensor還有個小細節,就是在IDE下,射線檢測碰撞的距離遠近會影響到繪製的線條顏色變化,以前我覺得只是單一的碰撞到檢測即變爲紅色,仔細發現並非。不過這個細節設計的難度應該不難,簡單想一下,應該是顯示的顏色=射線檢測的距離/射線可檢測的最大距離*顏色顯示的閾值範圍(RGB),我瞎猜的,估計差很少就這麼算的。
咱們下面來看一下Ray Perception Sensor Component 3D
組件的屬性各表明什麼意思:
Sensor Name:該Sensor的名字,相似於ID,注意在小藍身上的兩個名字不一樣哦,一個是RayPerceptionSensor,另外一個是OffsetRayPerceptionSensor。
Detectable Tags:設置射線能檢測到物體的Tag集合。這裏設置了3個Tag,分別表明了場景中的目標小白塊、目標區域和牆體。
Rays Per Direction:射線的條數,爲0的時候就只有一條向前的射線,詳情看下圖:
Max Ray Degrees:射線覆蓋的角度範圍,可看下圖:
Sphere Cast Radius:設置碰撞空心線球的半徑,以下圖:
Ray Length:設置射線投射的最遠距離。
Ray Layer Mask:設置射線能夠檢測到的Layer。
Observation Stacks:堆疊以前觀察的結果的數量,若設置爲1則表示不堆疊之前的觀察。
這裏插一下,在看這個屬性的時候,忽然發現前面環境與訓練參數中爲啥是70個觀測變量參數了,具體在RayPerceptionSensorComponentBase
腳本中有一個GetObservationShape()方法:
這裏numRays=7,numTags=3,則obsSize=(3+2)*7=35,而一個小藍身上又有兩個Sensor,則須要收集的觀測變量爲35*2=70個。
Start Vertical Offset:調整射線發出的角度,以下圖:
End Vertical Offset:相對於上面的屬性,這裏能夠設置射線尾部的角度,以下圖:
Debug Gizmos中的Ray Hit Color和Ray Miss Color則是分別設置射線碰撞檢測到物體線條的顏色以及未檢測到時線條的顏色。
好的,上面介紹了一下Ray Sensor 3D組件,咱們繼續回來,來看看一個訓練單元Area上其餘的物體。
Goal
目標區域,注意該區域並不僅是地面顯示的顏色,並且還帶有一個碰撞體,來使得小藍辨認以及小白碰撞區域檢測。
Block
小白塊,上面有一個Goal Detect.cs
腳本,用來檢測小白是否被推入目標區域。
Ground和WallsOuter
牆體和地面,沒什麼好說的~
這個示例相對來講比較簡單,咱們直接從小藍身上的Push Agent Basic
開始看起。
using System.Collections; using UnityEngine; using MLAgents; public class PushAgentBasic : Agent { //地面,須要更根據任務完成狀況更換地面材質 public GameObject ground; //整個訓練單元,開局時可能隨機改變角度 public GameObject area; [HideInInspector] public Bounds areaBounds;//地面碰撞體的邊界 //Push Block示例的設置,設置小藍的速度、旋轉角度等 PushBlockSettings m_PushBlockSettings; public GameObject goal;//目標區域 public GameObject block;//小白塊 [HideInInspector] public GoalDetect goalDetect;//小白塊上斷定進入目標區域腳本 public bool useVectorObs;//該選項應該是以前留下的,此代碼中無用 Rigidbody m_BlockRb; //小白塊剛體 Rigidbody m_AgentRb; //小藍剛體 Material m_GroundMaterial; //地面原始材質 //地面Renderer Renderer m_GroundRenderer; void Awake() { //找到Push Block初始配置 m_PushBlockSettings = FindObjectOfType<PushBlockSettings>(); } /// <summary> /// 初始化Agent /// </summary> public override void InitializeAgent() { base.InitializeAgent(); //設置小白腳本里的agent爲小藍 goalDetect = block.GetComponent<GoalDetect>(); goalDetect.agent = this; m_AgentRb = GetComponent<Rigidbody>(); m_BlockRb = block.GetComponent<Rigidbody>(); areaBounds = ground.GetComponent<Collider>().bounds; //拿到地面的的renderer,使得當小藍完成任務或未完成任務時替換地面材質 m_GroundRenderer = ground.GetComponent<Renderer>(); //緩存地面原始材質 m_GroundMaterial = m_GroundRenderer.material; //設置可變參數 SetResetParameters(); } /// <summary> /// 設置可變參數 /// </summary> public void SetResetParameters() { //設置地面的兩個可變參數:動摩擦係數+靜摩擦係數 SetGroundMaterialFriction(); //設置小白塊的兩個可變參數:小白的X、Z方向的比例大小以及小白的所受空氣阻力 SetBlockProperties(); } /// <summary> /// 設置地面可變參數 /// </summary> public void SetGroundMaterialFriction() { var resetParams = Academy.Instance.FloatProperties; var groundCollider = ground.GetComponent<Collider>(); groundCollider.material.dynamicFriction = resetParams.GetPropertyWithDefault("dynamic_friction", 0); groundCollider.material.staticFriction = resetParams.GetPropertyWithDefault("static_friction", 0); } /// <summary> /// 設置小白可變參數 /// </summary> public void SetBlockProperties() { var resetParams = Academy.Instance.FloatProperties; var scale = resetParams.GetPropertyWithDefault("block_scale", 2); //小白的比例 m_BlockRb.transform.localScale = new Vector3(scale, 0.75f, scale); //小白的空氣摩擦阻力 m_BlockRb.drag = resetParams.GetPropertyWithDefault("block_drag", 0.5f); } }
以上代碼沒什麼特別要講的,只用注意以前沒詳細講過的一個函數GetPropertyWithDefault(string key,float defaultValue)
,這個函數通常用於可變參數的設置,第一個是你要設置的可變參數的key,這個與你要訓練的可變參數配置文件中的部分一一對應,以下圖是當時在ML-Agents(四)3DBall補充の引入泛化中對3D Ball裏的可變參數訓練文件:
resampling-interval: 5000 mass: sampler-type: "uniform" min_value: 0.5 max_value: 10 gravity: sampler-type: "uniform" min_value: 7 max_value: 12 scale: sampler-type: "uniform" min_value: 0.75 max_value: 3
裏面的mass、gravity和scale就是在程序中設置的key一一對應。固然若是沒有訓練時,這個key所對應的值爲空,則GetPropertyWithDefault(string key,float defaultValue)
會取第二個默認參數。
重置代碼有一點有意思的地方,先上源碼:
/// <summary> /// Agent重置,在Done()時會自動調用 /// </summary> public override void AgentReset() { //使整個平臺(訓練單元)隨機旋轉角度0,90,180,270 var rotation = Random.Range(0, 4); var rotationAngle = rotation * 90f; area.transform.Rotate(new Vector3(0f, rotationAngle, 0f)); //重置小白 ResetBlock(); //重置小藍 transform.position = GetRandomSpawnPos(); m_AgentRb.velocity = Vector3.zero; m_AgentRb.angularVelocity = Vector3.zero; //重置可變參數 SetResetParameters(); } /// <summary> /// 當小藍推進小白到目標區域時調用,注意這裏調用是小白的腳本GoalDetect.cs調用的 /// </summary> public void ScoredAGoal() { //到達目標區域,獎勵5 AddReward(5f); //調用Agent的Done()函數,此時AgentReset()函數會自動執行 Done(); //設置地面亮綠色0.5秒 StartCoroutine(GoalScoredSwapGroundMaterial (m_PushBlockSettings.goalScoredMaterial, 0.5f)); } /// <summary> /// 設置地面材質爲綠色 /// </summary> IEnumerator GoalScoredSwapGroundMaterial(Material mat, float time) { m_GroundRenderer.material = mat;//地面設置爲綠色 yield return new WaitForSeconds(time); //等待0.5s後,換回原先材質 m_GroundRenderer.material = m_GroundMaterial; } /// <summary> /// 重置小白的位置和速度 /// </summary> void ResetBlock() { //小白的位置隨機重置 block.transform.position = GetRandomSpawnPos(); //使小白的速度置零 m_BlockRb.velocity = Vector3.zero; //使小白的角速度置零 m_BlockRb.angularVelocity = Vector3.zero; } /// <summary> /// 利用地面的Bounds範圍以及碰撞關係來隨機生成小白以及小藍出現的位置 /// </summary> public Vector3 GetRandomSpawnPos() { var foundNewSpawnLocation = false; var randomSpawnPos = Vector3.zero; while (foundNewSpawnLocation == false) { //隨機找兩個值X、Z,注意這裏的m_PushBlockSettings.spawnAreaMarginMultiplier係數 var randomPosX = Random.Range(-areaBounds.extents.x * m_PushBlockSettings.spawnAreaMarginMultiplier, areaBounds.extents.x * m_PushBlockSettings.spawnAreaMarginMultiplier); var randomPosZ = Random.Range(-areaBounds.extents.z * m_PushBlockSettings.spawnAreaMarginMultiplier, areaBounds.extents.z * m_PushBlockSettings.spawnAreaMarginMultiplier); //判斷隨機的位置附近是否存在碰撞體,若存在則繼續隨機生成位置, //直到生成的位置附近沒有其餘碰撞體 randomSpawnPos = ground.transform.position + new Vector3(randomPosX, 1f, randomPosZ); if (Physics.CheckBox(randomSpawnPos, new Vector3(2.5f, 0.01f, 2.5f)) == false) { foundNewSpawnLocation = true; } } return randomSpawnPos; }
以上代碼大部分都很簡單,每次訓練完成的條件就是小白被推入目標區域,而後調用小藍身上的ScoredAGoal()
函數,使得整個訓練重置。
有意思的是最後一部分GetRandomSpawnPos()
方法,即隨機生成小藍和小白位置方法。首先先看這個代碼前半部分,隨機的X、Z值除了隨機生成,還乘以了m_PushBlockSettings.spawnAreaMarginMultiplier參數,該參數是PushBlockSettings.cs
腳本中的,有了它的相乘,則規定了小白和小藍出現的位置佔場地的百分比,能夠參考下圖來理解:
圖中的數字大概就是m_PushBlockSettings.spawnAreaMarginMultiplier參數的取值,源碼中該值設置爲0.5,則小藍和小白出現的位置就在場地中心50%方形區域範圍內。這裏若是值越大,相應訓練的時間也就越長,也符合常理。
其次,在該代碼中還有一個while()循環,這個循環主要是判斷隨機生成的位置周圍是否有其它碰撞體(包括牆體等)。
該段代碼使用了Physics.CheckBox(Vector3 center, Vector3 halfExtents, Quaternion orientation = Quaternion.identity, int layermask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal)
函數,固然這個函數參數不少,源碼中就設置了前兩個值,意思就是在center位置生成一個碰撞盒,長寬高分別爲halfExtents(x、y、z)的兩倍。以下圖:
如上圖,碰撞盒的尺寸爲(5,0.02,5),若小白在圖示位置,而小藍一次隨機的位置在(0,0,0)點處,利用Physics.CheckBox()方法就檢測出了在(0,0,0)點處周圍有小白,所以這次生成的位置(0,0,0)並不能符合要求,而後就繼續while()去從新生成位置,知道小藍和小白離得足夠遠。
這裏比較坑的時,注意不能把m_PushBlockSettings.spawnAreaMarginMultiplier這個值設置的過小,過小的話你會發現程序一運行就卡死。也能夠想到,若是過小,while會變成死循環,由於沒有足夠的距離使得小白和小藍的距離足夠遠而結束循環。
Agent動做反饋代碼較爲簡單:
/// <summary> /// Agent每步動做反饋 /// </summary> public override void AgentAction(float[] vectorAction) { //利用矢量空間向量移動Agent MoveAgent(vectorAction); //每步都給予懲罰使得Agent迅速完成任務,這裏是-1/5000每步 AddReward(-1f / maxStep); } /// <summary> /// 根據矢量動做空間action[]來使得Agent做出反應 /// </summary> public void MoveAgent(float[] act) { var dirToGo = Vector3.zero;//向前向量 var rotateDir = Vector3.zero;//轉向向量 var action = Mathf.FloorToInt(act[0]);//act[]取值範圍爲0~6,則action取值範圍爲0~6 switch (action) { case 1: dirToGo = transform.forward * 1f;//向後 break; case 2: dirToGo = transform.forward * -1f;//向前 break; case 3: rotateDir = transform.up * 1f;//向右轉 break; case 4: rotateDir = transform.up * -1f;//向左轉 break; case 5: dirToGo = transform.right * -0.75f;//向右平移 break; case 6: dirToGo = transform.right * 0.75f;//向左平移 break; } transform.Rotate(rotateDir, Time.fixedDeltaTime * 200f);//旋轉方向 m_AgentRb.AddForce(dirToGo * m_PushBlockSettings.agentRunSpeed, ForceMode.VelocityChange);//移動 }
至此,Agent腳本分析完畢。
剩下的腳本主要是在PushBlockSettings物體上的PushBlockSettings
腳本,該腳本主要能夠設置本示例的公共參數。
using UnityEngine; public class PushBlockSettings : MonoBehaviour { /// <summary> /// Agent的速度 /// </summary> public float agentRunSpeed; /// <summary> /// Agent的旋轉速度 /// </summary> public float agentRotationSpeed; /// <summary> /// 使用場地比例乘數,具體做用已在上一節中講述 /// </summary> public float spawnAreaMarginMultiplier; /// <summary> /// 任務達成時,地面要更換的材質 /// </summary> public Material goalScoredMaterial; /// <summary> /// 任務失敗時,地面要更換的材質(工程中未用到) /// </summary> public Material failMaterial; }
基於上一篇ML-Agents(七)訓練指令與訓練配置文件,咱們來大概研究一下Push Block的設置:
trainer_config.yaml
PushBlock: max_steps: 1.5e7 batch_size: 128 buffer_size: 2048 beta: 1.0e-2 hidden_units: 256 summary_freq: 60000 time_horizon: 64 num_layers: 2
默認的參數就先不看,主要看PushBlock獨有的設置。
max_steps
:設置的很大,說明該次訓練應該仍是比較費時間的,不過場景中也相應有32個訓練單元,因此也相應加快了訓練速度。batch_size
:相比默認1024設置的較小,由於這次訓練的矢量動做空間是離散的(Discrete Type),因此設置範圍爲32
-512
。buffer_size
:是batch_size
的倍數,範圍爲2048
-409600
。beta
:相比默認設置5.0e-3,該值設置更大,也就意味着在訓練過程當中agent的行動更具隨機性,不過若是entropy的降低太慢,則要減少該值。hidden_units
:默認設置爲128,該值設置爲256,緣由是此示例相對來講觀察變量之間的交互複雜度更高,所以該值須要更大。summary_freq
:該值設置多久保存一次統計數據,主要決定在tensorboard中顯示數據點的數量。因爲該訓練max_steps
較大,所以該值也相對默認值10000設置的大。time_horizon
:若是在一個episode裏,agent頻繁得到獎勵或episode數量很是大的狀況下,該值須要更小。對應於本示例訓練,因爲每一步都懲罰agent,要使其儘快完成訓練,所以該值設置較小(PS.該值範圍爲32-2048)。num_layers
:與默認相同,定義在觀察值輸入以後或在視覺觀察的CNN編碼後存在多少個隱藏層。對於簡單的問題,更少的層數可以使得訓練更加迅速和高效。對於更加複雜的控制問題,可能須要更多的層(PS.該值的範圍爲1-3)。關於配置文件更多的配置項能夠看我上一篇文章~
咱們先來使用官方不帶可變參數的配置來訓練一下,在命令行中輸入熟悉的訓練命令:
mlagents-learn config/trainer_config.yaml --run-id=PushBlock_Normal --train
進行訓練,以下圖:
剛開始小藍蠢的要死~還不知道本身來到這個世界上要幹啥。。。訓練一下子,看它能不能醒悟本身的任務。
等到一段時間後,會發現有些小藍好像明白了本身push小白的任務,但其實大多數小藍都只是把小白推到某個牆角就卡住了。這種狀況下,初期的小藍就只能等到這一次的最大動做數5000步後,重置來進行下一次嘗試。
OK,咱們仍是等到訓練一段時間後在來看效果。
隨着時間的退役,咱們能夠觀察到愈來愈多的訓練單元達成了任務:
同時也能夠從命令行中的Mean Reward
值逐漸增長來看到學習的效果越來愈好。
訓練效果愈來愈好,代表參數基本設置沒啥大問題,如今只須要等待訓練完畢便可。訓練大概3小時後,訓練完畢,能夠同時查看這次訓練的tensorboard。
將訓練模型放入Unity中,發現可使小藍正確push小白到目標區域中。
這次示例整體來講較爲簡單,主要就是學習關於射線傳感器的應用。本篇文章就此結束,歡迎你們留言交流~
寫文不易~所以作如下申明:
1.博客中標註原創的文章,版權歸原做者 煦陽(本博博主) 全部;
2.未經原做者容許不得轉載本文內容,不然將視爲侵權;
3.轉載或者引用本文內容請註明來源及原做者;
4.對於不遵照此聲明或者其餘違法使用本文內容者,本人依法保留追究權等。