ML-Agents案例之地牢逃脱

2024-04-03 18:32
文章标签 案例 ml agents 逃脱 地牢

本文主要是介绍ML-Agents案例之地牢逃脱,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本案例源自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案例之地牢逃脱的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/873666

相关文章

ROS2从入门到精通4-4:局部控制插件开发案例(以PID算法为例)

目录 0 专栏介绍1 控制插件编写模板1.1 构造控制插件类1.2 注册并导出插件1.3 编译与使用插件 2 基于PID的路径跟踪原理3 控制插件开发案例(PID算法)常见问题 0 专栏介绍 本专栏旨在通过对ROS2的系统学习,掌握ROS2底层基本分布式原理,并具有机器人建模和应用ROS2进行实际项目的开发和调试的工程能力。 🚀详情:《ROS2从入门到精通》 1 控制插

django学习入门系列之第三点《案例 小米商城头标》

文章目录 阴影案例 小米商城头标往期回顾 阴影 设置阴影 box-shadow:水平方向 垂直方向 模糊距离 颜色 box-shadow: 5px 5px 5px #aaa; 案例 小米商城头标 目标样式: CSS中的代码 /*使外边距等于0,即让边框与界面贴合*/body{margin: 0;}/*控制父级边框*/.header{backgroun

MATLAB算法实战应用案例精讲-【数模应用】三因素方差

目录 算法原理 SPSSAU 三因素方差案例 1、背景 2、理论 3、操作 4、SPSSAU输出结果 5、文字分析 6、剖析 疑难解惑 均方平方和类型? 事后多重比较的类型选择说明? 事后多重比较与‘单独进行事后多重比较’结果不一致? 简单效应是指什么? 边际估计均值EMMEANS是什么? 简单简单效应? 关于方差分析时的效应量? SPSSAU-案例 一、案例

Retrofit介绍案例

Retrofit这东西我就不多做解释了,反正最近应用很广,基本都快和OkHttp一起成为安卓的事实网络访问标准框架了。   这么好的一个东西,官网文档实在是不算太好,说的不太清晰。按官网的经常会有“Could not locate ResponseBody converter for”问题。 反正折腾了一番,终于跑出来了一个例子。这里把正确的例子写出来,方便大家参考。 首先要注意

Kimichat使用案例026:AI翻译英语PDF文档的3种方法

文章目录 一、介绍二、腾讯交互翻译TranSmart https://transmart.qq.com/三、沉浸式翻译三、谷歌网页翻译 一、介绍 短的文章,直接丢进kimichat、ChatGPT里面很快就可以翻译完成,而且效果很佳。但是,很长的PDF文档整篇需要翻译,怎么办呢? 二、腾讯交互翻译TranSmart https://transmart.qq.com/ 软件

用python写一个AI Agent对接企业微信上下游协同的案例

要实现一个AI Agent对接企业微信上下游协同,我们可以使用Python编写一个企业微信机器人,用于接收和处理来自企业微信的消息。在此示例中,我们将使用`wechatpy`库来实现企业微信机器人,并使用`requests`库实现与上下游系统的通信。 首先,确保安装了`wechatpy`和`requests`库: ```bash pip install wechatpy requests ``

[案例解析]山东首单跨境数据资产入表案例解析

“ 该案例实现了数据资产跨境的突破” 众所周知,自从我国《个护法》出台,加上后来对于数据出海的各种规定陆续出台,数据出海面临更加严格的监管,能够出海已经不容易,再能够在出海的基础上实现数据资产入表更是意义重大。 01   案例简介 —————————————————— 近日,在济南市大数据局、中国(山东)自贸试验区济南片区的指导下,山东产权交易集团旗下山

React18中各种Hooks用法总结( 内附案例讲解)

React中各种Hooks用法总结 内附案例讲解 一、useState useState 是一个 React Hook,它允许你向组件添加一个 状态变量。 import React, { FC, memo, useState } from 'react'import { MainContainer } from './style'interface IProps {children?:

《三国:谋定天下》成为了SLG游戏现象级的成功案例

原标题:《三国:谋定天下》引领SLG游戏新潮流,B站股价五个飙升了30%   易采游戏网6月23日:B站作为年轻人喜爱的文化社区和视频平台,再次用一款新的游戏证明了其在游戏发行领域的独到眼光与强大实力。最近大火的策略角色扮演游戏《三国:谋定天下》成为了现象级的成功案例,不仅游戏本身质量受到认可,而且在竞争激烈的iOS畅销榜上勇夺第三的位置,仅排在了资深巨头DNF手游和《王者荣耀》之后。更加引人注

JUnit最简单的测试案例

需要测试的类: package cn.edu.junit;public class Calculate {//加public int add(int a,int b){return a+b;}//减public int subtract(int a,int b){return a-b;}//乘public int multiply(int a,int b){return a*b;}//除pu