本案例源自ML-Agents官方的示例,Github地址:https://github.com/Unity-Technologies/ml-agents,本文是详细的配套讲解。

本文基于我前面发的两篇文章,需要对ML-Agents有一定的了解,详情请见:Unity强化学习之ML-Agents的使用、ML-Agents命令及配置大全。

我前面的相关文章有:

ML-Agents案例之Crawler

ML-Agents案例之推箱子游戏

ML-Agents案例之跳墙游戏

ML-Agents案例之食物收集者

ML-Agents案例之双人足球

Unity人工智能之不断自我进化的五人足球赛

环境说明

  • 设置:特工被困在一个有龙的地牢中,必须共同努力才能逃脱。为了取回钥匙,其中一名特工必须找到并杀死龙,为此牺牲自己。龙会掉落一把钥匙供其他人使用。然后其他特工可以拿起这把钥匙并打开地牢门。如果特工花费的时间过长,龙将通过传送门逃跑并且环境会重置。

  • 目标:打开地牢门并离开。

  • 任何智能体成功打开门并离开地牢,则 +1 团队奖励。

  • 此项目的训练难点在于,智能体为了团队奖励,必须学会牺牲自己。

  • 输入:智能体的输入包含一个射线传感器RayPerceptionSensor3D,识别的标签分别为墙,队友,龙,钥匙,门锁,龙的洞穴。共15根射线,参数见下方图片。关于该传感器的详细说明见ML-Agents案例之推箱子游戏。

    除了传感器之外,程序中还加入了一项检测智能体身上是否有钥匙的输入。

  • 输出:智能体只采取了一项的离散输出,其中这个离散输出包含七个只,代表什么都不做、前进、后退、向左走、向右转、向左转、向右转。较少的输出会大大降低神经网络复杂度,减少训练时间。缺点是同一时间只能执行一个动作,降低智能体的灵活性,例如不能同时前进和旋转,也不能前进和向右转等。

代码讲解

首先是标准的三件套Behavior Parameters、Decision Requester 、Model Overrider。其中只有Behavior Parameters需要调参数,设置见上图。以前已详细讲解了各自的作用,这里不再讲解。

现在看看主要的智能体代码PushAgentEscape.cs

初始化方法Initialize():

public override void Initialize()
{// 获取组件m_GameController = GetComponentInParent<DungeonEscapeEnvController>();m_AgentRb = GetComponent<Rigidbody>();m_PushBlockSettings = FindObjectOfType<PushBlockSettings>();// 默认没有钥匙MyKey.SetActive(false);IHaveAKey = false;
}

每一个episode开始时的处理OnEpisodeBegin()方法:

public override void OnEpisodeBegin()
{MyKey.SetActive(false);IHaveAKey = false;
}

状态输入CollectObservations方法:

public override void CollectObservations(VectorSensor sensor)
{sensor.AddObservation(IHaveAKey);
}

可以看到除了传感器的输入之外,这里只有是否拥有钥匙一个输入。

动作输出OnActionReceived方法:

public override void OnActionReceived(ActionBuffers actionBuffers)
{MoveAgent(actionBuffers.DiscreteActions);
}public void MoveAgent(ActionSegment<int> act)
{var dirToGo = Vector3.zero;var rotateDir = Vector3.zero;var action = act[0];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);
}

可以看到这里只有一个离散输出,包含0-6七个值,其中0为什么都不做。

碰撞检测:

碰撞检测分为两个部分,其中洞穴,龙,门锁是碰撞体,调用的是OnCollisionEnter方法:

void OnCollisionEnter(Collision col)
{// 当身上有钥匙,碰到锁,那么门打开,同时消耗钥匙,调用UnlockDoor方法if (col.transform.CompareTag("lock")){       if (IHaveAKey){MyKey.SetActive(false);IHaveAKey = false;m_GameController.UnlockDoor();}}// 当碰到龙时,销毁身上的钥匙(实际上身上此时不可能有钥匙,为了逻辑完整这样写),并且调用KilledByBaddie方法if (col.transform.CompareTag("dragon")){m_GameController.KilledByBaddie(this, col);MyKey.SetActive(false);IHaveAKey = false;}// 当碰到洞穴时,调用TouchedHazard方法if (col.transform.CompareTag("portal")){m_GameController.TouchedHazard(this);}
}

另一部分是钥匙,它被设定为触发器而非碰撞体,调用的是OnTriggerEnter方法:

void OnTriggerEnter(Collider col)
{// 如果钥匙是和智能体在同一个父物体下并且智能体为激活状态// 那么取消激活钥匙并激活身上的子物体钥匙,所以看起来像捡起来钥匙一样if (col.transform.CompareTag("key") && col.transform.parent == transform.parent &&                                      gameObject.activeInHierarchy){print("Picked up key");MyKey.SetActive(true);IHaveAKey = true;col.gameObject.SetActive(false);}
}

如果玩家想手动操控其中一个智能体,则需要在智能体没有模型的情况下重写Heuristic方法:

public override void Heuristic(in ActionBuffers actionsOut)
{var discreteActionsOut = actionsOut.DiscreteActions;if (Input.GetKey(KeyCode.D)){discreteActionsOut[0] = 3;}else if (Input.GetKey(KeyCode.W)){discreteActionsOut[0] = 1;}else if (Input.GetKey(KeyCode.A)){discreteActionsOut[0] = 4;}else if (Input.GetKey(KeyCode.S)){discreteActionsOut[0] = 2;}
}

下面讲解控制整个环境的脚本DungeonEscapeEnvController.cs

脚本先定义了智能体和恶龙所拥有的信息类,把关键信息封装起来便于调用,使得代码更加简洁美观:

// 定义智能体信息类
public class PlayerInfo
{// 智能体脚本public PushAgentEscape Agent;// 智能体起始位置public Vector3 StartingPos;// 智能体起始旋转向量public Quaternion StartingRot;// 智能体刚体public Rigidbody Rb;// 智能体碰撞体public Collider Col;
}// 定义龙信息类
public class DragonInfo
{// 龙的脚本public SimpleNPC Agent;// 龙的起始位置public Vector3 StartingPos;// 龙的其实旋转向量public Quaternion StartingRot;// 龙的刚体public Rigidbody Rb;// 龙的碰撞体public Collider Col;// 起始的Transformpublic Transform T;// 是否死亡public bool IsDead;
}

然后定义了一系列的变量:

// 每一个episode的最大步数和最大时间,超过两者环境会重置
[Header("Max Environment Steps")] public int MaxEnvironmentSteps = 25000;
private int m_ResetTimer;
// 区域大小
public Bounds areaBounds;
// 地面
public GameObject ground;
// 地面材质
Material m_GroundMaterial;
// 地面渲染
Renderer m_GroundRenderer;
// 智能体信息列表
public List<PlayerInfo> AgentsList = new List<PlayerInfo>();
// 龙的信息列表
public List<DragonInfo> DragonsList = new List<DragonInfo>();
// 建立一个字典,键为智能体脚本,值为智能体信息
private Dictionary<PushAgentEscape, PlayerInfo> m_PlayerDict = new Dictionary<PushAgentEscape, PlayerInfo>();
// 是否随机智能体的位置和旋转
public bool UseRandomAgentRotation = true;
public bool UseRandomAgentPosition = true;
// 把推方块的脚本拿过来复用了,名字都没改
PushBlockSettings m_PushBlockSettings;
// 存货的智能体数量
private int m_NumberOfRemainingPlayers;
// 钥匙
public GameObject Key;
// 墓碑
public GameObject Tombstone;
// 智能体组(重中之重)
private SimpleMultiAgentGroup m_AgentGroup;

然后就是对场景初始化,调用的Start方法:

void Start()
{// 获取地面界限areaBounds = ground.GetComponent<Collider>().bounds;// 获取地面渲染,方便改变材质m_GroundRenderer = ground.GetComponent<Renderer>();// 初始材质m_GroundMaterial = m_GroundRenderer.material;// 获取全局设定脚本m_PushBlockSettings = FindObjectOfType<PushBlockSettings>();// 重新计算场上存在的智能体m_NumberOfRemainingPlayers = AgentsList.Count;// 隐藏钥匙Key.SetActive(false);// 给列表中的智能体添加上对应的信息,并把智能体添加到组中,同一组的智能体会相互合作m_AgentGroup = new SimpleMultiAgentGroup();foreach (var item in AgentsList){item.StartingPos = item.Agent.transform.position;item.StartingRot = item.Agent.transform.rotation;item.Rb = item.Agent.GetComponent<Rigidbody>();item.Col = item.Agent.GetComponent<Collider>();// 添加到组m_AgentGroup.RegisterAgent(item.Agent);}// 给龙列表中的龙添加信息foreach (var item in DragonsList){item.StartingPos = item.Agent.transform.position;item.StartingRot = item.Agent.transform.rotation;item.T = item.Agent.transform;item.Col = item.Agent.GetComponent<Collider>();}// 重置场景ResetScene();
}

在ResetScene中:

 void ResetScene(){// 重置计时m_ResetTimer = 0;// 重置生存的智能体数量m_NumberOfRemainingPlayers = AgentsList.Count;// 四个方向任意旋转场景,可以防止过拟合在一个位置上var rotation = Random.Range(0, 4);var rotationAngle = rotation * 90f;transform.Rotate(new Vector3(0f, rotationAngle, 0f));// 重置列表中的每个智能体foreach (var item in AgentsList){// 如果设定了随机,在场景中随机一个位置,没有就固定位置var pos = UseRandomAgentPosition ? GetRandomSpawnPos() : item.StartingPos;var rot = UseRandomAgentRotation ? GetRandomRot() : item.StartingRot;        item.Agent.transform.SetPositionAndRotation(pos, rot);// 状态都清零item.Rb.velocity = Vector3.zero;item.Rb.angularVelocity = Vector3.zero;item.Agent.MyKey.SetActive(false);item.Agent.IHaveAKey = false;item.Agent.gameObject.SetActive(true);// 这一行我认为可以去掉,无需再次添加m_AgentGroup.RegisterAgent(item.Agent);}// 重置钥匙Key.SetActive(false);// 重置墓碑Tombstone.SetActive(false);// 重置列表中的每一只龙foreach (var item in DragonsList){if (!item.Agent){return;}// 设定固定的起始位置item.Agent.transform.SetPositionAndRotation(item.StartingPos, item.StartingRot);// 设定随机的行走速度item.Agent.SetRandomWalkSpeed();// 激活智能体item.Agent.gameObject.SetActive(true);}}

在获取任意场景中位置的时候,调用的是GetRandomSpawnPos,这段代码可复用很强

public Vector3 GetRandomSpawnPos()
{var foundNewSpawnLocation = false;var randomSpawnPos = Vector3.zero;while (foundNewSpawnLocation == false){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;
}

接下来是每0.02秒都执行一次的FixedUpdate方法:

这里主要检测一个episode是否已经到达了设定的时间和最大步数,满足两者则环境重置。

void FixedUpdate()
{m_ResetTimer += 1;if (m_ResetTimer >= MaxEnvironmentSteps && MaxEnvironmentSteps > 0){m_AgentGroup.GroupEpisodeInterrupted();ResetScene();}
}

接下来定义了三个对应接触龙,接触洞穴,接触门锁的方法:

当智能体接触洞穴时:

public void TouchedHazard(PushAgentEscape agent)
{// 智能体死亡,数量-1,数量为0时重置环境m_NumberOfRemainingPlayers--;if (m_NumberOfRemainingPlayers == 0 || agent.IHaveAKey){m_AgentGroup.EndGroupEpisode();ResetScene();}else{agent.gameObject.SetActive(false);}
}

当智能体接触门锁时:

public void UnlockDoor()
{// 获得集体奖励m_AgentGroup.AddGroupReward(1f);// 改变地面材质0.5秒StartCoroutine(GoalScoredSwapGroundMaterial(m_PushBlockSettings.goalScoredMaterial, 0.5f));print("Unlocked Door");// 结束游戏m_AgentGroup.EndGroupEpisode();// 重置场景ResetScene();
}

当智能体接触龙时:

public void KilledByBaddie(PushAgentEscape agent, Collision baddieCol)
{// 龙被杀死,隐藏baddieCol.gameObject.SetActive(false);// 一个智能体死亡,隐藏m_NumberOfRemainingPlayers--;agent.gameObject.SetActive(false);print($"{baddieCol.gameObject.name} ate {agent.transform.name}");// 激活墓碑Tombstone.transform.SetPositionAndRotation(agent.transform.position, agent.transform.rotation);Tombstone.SetActive(true);// 激活钥匙Key.transform.SetPositionAndRotation(baddieCol.collider.transform.position, baddieCol.collider.transform.rotation);Key.SetActive(true);
}

此处可以试试扣除接触龙智能体本身的分数,看看智能体是否舍己为人,牺牲自己的分数换取团队的收益。

改变地面材质的携程:

IEnumerator GoalScoredSwapGroundMaterial(Material mat, float time)
{m_GroundRenderer.material = mat;yield return new WaitForSeconds(time); // Wait for 2 secm_GroundRenderer.material = m_GroundMaterial;
}

以下是NPC龙的代码,很简单,只有移动的逻辑:

using UnityEngine;public class SimpleNPC : MonoBehaviour
{public Transform target;private Rigidbody rb;public float walkSpeed = 1;private Vector3 dirToGo;// 比Start更早执行void Awake(){rb = GetComponent<Rigidbody>();}void Update(){}// 每0.02秒执行一次void FixedUpdate(){dirToGo = target.position - transform.position;dirToGo.y = 0;rb.rotation = Quaternion.LookRotation(dirToGo);// 执行移动rb.MovePosition(transform.position + transform.forward * walkSpeed * Time.deltaTime);}// 设置一个随机速度public void SetRandomWalkSpeed(){walkSpeed = Random.Range(1f, 7f);}
}

在龙下还挂着一个脚本,用来检测龙是否接触到洞穴:

using UnityEngine;
using UnityEngine.Events;namespace Unity.MLAgentsExamples
{public class CollisionCallbacks : MonoBehaviour{// 以下定义了多个事件,需要在Unity编辑器中订阅它们[System.Serializable]public class TriggerEvent : UnityEvent<Collider>{}[Header("Trigger Callbacks")]public TriggerEvent onTriggerEnterEvent = new TriggerEvent();// 这个案例只用到了这个方法,其他方法都没有订阅private void OnCollisionEnter(Collision col){if (col.transform.CompareTag(tagToDetect)){onCollisionEnterEvent.Invoke(col, transform);}      }
}

订阅事件:

其中执行的方法如下:

public void BaddieTouchedBlock()
{m_AgentGroup.EndGroupEpisode();StartCoroutine(GoalScoredSwapGroundMaterial(m_PushBlockSettings.failMaterial, 0.5f));ResetScene();
}

配置文件

最简单的配置:

behaviors:DungeonEscape:trainer_type: pocahyperparameters:batch_size: 1024buffer_size: 10240learning_rate: 0.0003beta: 0.01epsilon: 0.2lambd: 0.95num_epoch: 3learning_rate_schedule: constantnetwork_settings:normalize: falsehidden_units: 256num_layers: 2vis_encode_type: simplereward_signals:extrinsic:gamma: 0.99strength: 1.0keep_checkpoints: 5max_steps: 20000000time_horizon: 64summary_freq: 60000

效果演示

后记

这一个案例是多智能体案例,探索了智能体自我牺牲以求团队利益的可能性,以后可以以此为依据,做一个更为复杂的解密类游戏,其中包含人类想不到的解密方法,但智能体可以学习出来,这对于奖励函数的设置是一个巨大的挑战。

ML-Agents案例之地牢逃脱相关推荐

  1. 2017网易校招真题 地牢逃脱

    地牢逃脱 时间限制:1秒 空间限制:32768K 热度指数:8622 算法知识视频讲解 题目描述 给定一个 n 行 m 列的地牢,其中 '.' 表示可以通行的位置,'X' 表示不可通行的障碍,牛牛从 ...

  2. Python技术栈与Spark交叉数据分析双向整合技术实战--大数据ML样本集案例实战

    版权声明:本套技术专栏是作者(秦凯新)平时工作的总结和升华,通过从真实商业环境抽取案例进行总结和分享,并给出商业应用的调优建议和集群环境容量规划等内容,请持续关注本套博客.QQ邮箱地址:1120746 ...

  3. 地牢逃脱(BFS(广度优先搜索))

    题目描述 给定一个 n 行 m 列的地牢,其中 '.' 表示可以通行的位置,'X' 表示不可通行的障碍,牛牛从 (x0 , y0 ) 位置出发,遍历这个地牢,和一般的游戏所不同的是,他每一步只能按照一 ...

  4. 算法题(四十):BFS解决网易2017年笔试题——地牢逃脱

    题目描述 给定一个 n 行 m 列的地牢,其中 '.' 表示可以通行的位置,'X' 表示不可通行的障碍,牛牛从 (x0 , y0 ) 位置出发,遍历这个地牢,和一般的游戏所不同的是,他每一步只能按照一 ...

  5. 地牢逃脱-python

    题目描述 给定一个 n 行 m 列的地牢,其中 '.' 表示可以通行的位置,'X' 表示不可通行的障碍,牛牛从 (x0 , y0 ) 位置出发,遍历这个地牢,和一般的游戏所不同的是,他每一步只能按照一 ...

  6. 网易笔试题:地牢逃脱

    给定一个 n 行 m 列的地牢,其中 '.' 表示可以通行的位置,'X' 表示不可通行的障碍,牛牛从 (x0 , y0 ) 位置出发,遍历这个地牢,和一般的游戏所不同的是,他每一步只能按照一些指定的步 ...

  7. ML-Agents案例之蠕虫

    本案例源自ML-Agents官方的示例,Github地址:https://github.com/Unity-Technologies/ml-agents,本文是详细的配套讲解. 本文基于我前面发的两篇 ...

  8. 热门 | Google Brain前员工深度盘点2017人工智能和深度学习各大动态

    翻译 | AI科技大本营 参与 | shawn 编辑 | Donna 2017年是人工智能井喷的一年.Google Brain团队前成员Denny Britz在自己的博客WILDML上对过去一年人工智 ...

  9. 剑指Offer——滴滴笔试题+知识点总结

    文章目录 一.情景回顾 二.选择题 三.Linux链接 四.编程题 4.1 小青蛙走迷宫 4.2 末尾0的个数 五.简答题 六.Tomcat Server处理http请求的过程 一.情景回顾 时间:2 ...

最新文章

  1. python使用matplotlib可视化3D曲面图、曲面图表示一个指定的因变量y与两个自变量x和z之间的函数关系
  2. 进阶学习(3.12) Operand Pattern 装饰器模式
  3. 【机器学习PAI实践八】用机器学习算法评估学生考试成绩
  4. ABAP性能优化之向内表添加纪录
  5. springbatch的reader,如何使用mybatis?使用MyBatisCursorItemReader可以完美解决
  6. 数据库面试题【三、索引有B+索引和hash索引】
  7. Ubuntu版本介绍
  8. 《c语言从入门到精通》看书笔记——第6章 选择结构程序设计
  9. P4570 [BJWC2011]元素(线性基+贪心)
  10. A - C语言实验——最值
  11. 一次SpringBoot AutoWired 注入服务为null的事件
  12. HDU 1068 Girls and Boys(最大独立集合 = 顶点数 - 最大匹配数)
  13. 《TensorFlow 2.0深度学习算法实战教材》学习笔记(五、神经网络)
  14. 阶段1 语言基础+高级_1-3-Java语言高级_07-网络编程_第3节 综合案例_文件上传_3_综合案例_文件上传案例的服务器端...
  15. 关于Adapter模式
  16. [POJ2796]Feel Good
  17. ubuntu 常用命令锦集
  18. 【AR开发】ARCore简介
  19. Bugku:杂项 一枝独秀
  20. linux读取class下的文件路径,Class类getResource方法获取文件路径

热门文章

  1. ppt怎么转换成pdf
  2. linux脚本获取经纬度,JS实现根据详细地址获取经纬度功能示例
  3. java项目pm_项目中PO、PM的职责区分
  4. 企业引入自动化以打造完善的客户体验
  5. 在python做对应分析_案例6:SPSS--对应分析
  6. Meta Reality Labs:理想的VR头显还需克服这10项技术挑战
  7. matlab之运动目标检测
  8. JavaScript的富文本编辑
  9. 【KnowingAI知智】S4语音任务|知识卡片|笔记合集
  10. 最新版校园招聘进大厂系列----------(1)阿里篇 -----未完待续