ML Agents 学习笔记 (1)
本文是对 https://developer.unity.cn/projects/6232aab0edbc2a0019dcfe38 的补充, 非原创.
0. 环境搭建
创建虚拟环境, 环境内安装 ml-agents 包等.
安装 Unity, 克隆 ML-Agents github 仓库至本地.
1. 打开场景并运行
用 Unity 打开 Github clone 下来的项目; 具体就是打开 Unity Hub, 选择 Open (而不是 New Project), 打开 clone 下来的储存库的 Project 文件夹.
打开官方示例工程里面 \Assets\ML-Agents\Examples\Basic\Scenes
路径下的 scene (如图), 运行后我们可以看到AI不断的向更大的球的方向移动.
2. 更改大小球位置
我们可以尝试编辑 AI 身上的 BasicController 脚本 (脚本位置在 \Assets\ML-Agents\Examples\Basic\Scripts
文件夹下),将里面的 k_SmallGoalPosition
的值改为 17,k_LargeGoalPosition
的值改为 7 (也就是将大球和小球之间的位置互换),再运行场景.
可以看到AI并没有按照需求去朝着更大的球的方向移动,而是选择朝右边移动.
通过代码可以发现, 提供的 example AI 只知道自己的位置, 并不知道球的位置; 而因为训练时球的位置是固定的, 所以AI得出了 "一直向右走就能碰到大球" 的结论.
因此, 当环境改变时AI并没有改变移动方向.
3. 创建一个自己的环境
- 新建一个 Unity 项目, 不妨叫 To Large Goal.
- 打开项目, 执行以下步骤:
- 上方菜单栏选择 Window
- 选择 Package Manager
- 弹出窗口中选择加号, 然后选择 Add Package From Disk
- 你在前面 clone 下来的 ML-Agents 项目 (后面统一叫原项目) 的一级目录中会有一个叫 "com.unity.ml-agents" 的文件夹, 文件夹里有一个叫 "package.json" 的文件. 上一步不是选择了 Add Package From Disk 吗, 就在弹出窗口将这个 "package.json" 添加到你自己的项目 To Large Goal 中.
- 将原项目中
Project\Assets\ML-Agents\Examples
文件夹下的Basic
和SharedAssets
文件夹 (也就是上一节打开的 Basic 场景和共享的一些资源) 复制到新建项目 To Large Goal 中的\Assets
文件夹下. - 打开 To Large Goal, 先打开 Baisc Scene, 然后将 Scene 中原有的 Basic 对象从 Hierarchy 导航栏删除.
- 在 Project 导航栏中, 点开
\Assets\ML-Agents\Basic\Prefabs\Basic
, 删除预设体中 BasicAgent 下已经添加的所有脚本. - 在 Project 导航栏中, 删除
\Assets\ML-Agents\Basic\Scripts
文件夹下的所有脚本.
4. 编写自己的脚本
4.1 创建脚本
在 \Assets\ML-Agents\Examples\Basic\Scripts
下新建 cs 脚本 ToLargeGoal.cs, 写入以下代码:
using System.Collections;
using System.Collections.Generic;
using Unity.MLAgents;
using Unity.MLAgents.Actuators;
using Unity.MLAgents.Sensors;
using UnityEngine;
public class ToLargeGoal : Agent
{
public override void OnEpisodeBegin()
{
// ...
}
private void RandomGoalPosition()
{
// ...
}
public override void CollectObservations(VectorSensor sensor)
{
// ...
}
public override void OnActionReceived(ActionBuffers actions)
{
// ...
}
private void CalculateDistance()
{
// ...
}
public override void Heuristic(in ActionBuffers actionsOut)
{
// ...
}
public Transform largeGoal;
public Transform smallGoal;
public float ballDistance;
public float moveSpeed;
}
函数后面几节会实现.
把创建好的脚本添加到预制体 Basic 中的 BasicAgent 上面 (一定是 Prefab 文件夹下的 Basic 预制体),
我们会发现除了我们自己的脚本, 还多添加了一个名叫 BehaviorParameters 的脚本组件;
每一个 Agent 都会有一个适合自己的 BehaviorParameters, 其内部参数我们会在后面用到时提到.
4.2 设置环境 - OnEpisodeBegin()
重写 OnEpisodeBegin()
函数.
该函数是在每次训练开始时调用, 可以用来初始化AI和环境.
我们在这里重置AI的位置并且随机球的位置, 其中 ballDistance
用来控制球和 AI 小人的距离, 用较小的距离会让 AI 训练的更快.
ballDistance
, largeGoal
, smallGoal
和 moveSpeed
后面都会在 Unity 中手动分配初始化值, 所以脚本中没有初始化.
// Inside class MyFirstAgent
public override void OnEpisodeBegin()
{
RandomGoalPosition();
transform.localPosition = Vector3.zero;
}
private void RandomGoalPosition()
{
int status = Random.Range(0, 2);
switch (status)
{
case 0:
largeGoal.transform.localPosition = Vector3.right * ballDistance;
smallGoal.transform.localPosition = Vector3.left * ballDistance;
break;
default:
largeGoal.transform.localPosition = Vector3.left * ballDistance;
smallGoal.transform.localPosition = Vector3.right * ballDistance;
break;
}
}
4.3 设置所需观察信息 - CollectObservations(...)
重写 CollectObservations(VectorSensor sensor)
函数; 这个函数每走一步会收集 AI 所需要的矢量信息, 而AI通过这些信息了解现在的环境 (可以用 sensor.AddObservation()
方法收集所需数据).
当前情况下我们需要让AI知道 :
- 角色当前x轴坐标:
transform.localPosition.x
- 大球现在的x坐标:
largeGoal.localPosition.x
由于初始化环境时小球的位置不会挡住大球,所以我们不加入小球的位置.
// Inside class MyFirstAgent
public override void CollectObservations(VectorSensor sensor)
{
sensor.AddObservation(largeGoal.localPosition.x);
sensor.AddObservation(transform.localPosition.x);
}
之后需要在 BehaviorParameters 组件 (Prefab 文件夹下 Basic 对象中的 BasicAgent 的组件) 里面设置一下观测数组的大小和需要在多少帧内连续观察.
这里要注意, 如果 AddObservation()
方法里赋值的是三维向量, 就要算做观察了三次 (因为有x,y,z三个数据), 因此数组大小就要加三; 如果观察的是四元数的话数组大小就要加四.
例子中我们观察了两个 float 类型的值, 所以数组长度是2; 并且我们只需要观察大球当前的位置, 所以只需要观察 1 帧.
4.4 控制 AI 移动 - OnActionReceived(...)
重写 OnActionReceived(ActionBuffers actions)
函数, 这个函数用来指定每一步的AI的行为.
具体代码实现在下节和训练结束条件及奖励机制一起实现.
ML-Agents 支持两种类型的操作: 连续和离散.
- 可以通过
actions.ContinuousActions
数组来记录所有连续型的操作, 数组内的每一个值都在 [-1,1] 之间. - 可以通过
actions.DiscreteActions
数组记录所有离散的操作, 每个值应该是 0 到 x 中的整数, 而x 表示该操作的所有可能性的总数 (例如, 跳跃操作可以分为跳或不跳 (因此 x=2),如果是移动的话可以分为不动、上、下、左、右 (因此 x=5)).
当然 actions 只是提供了数据, 具体操作还是要代码来实现的.
对于连续型操作, 在本文案例中希望 AI 每一步的位移的差值都是连续的, 所以用 transform.position += Vector3.right * actions.ContinuousActions[0]* scale
;
至于离散型操作, 比如跳跃, 0 代表不跳, 1 代表跳跃,则当值为 1 时需要实现跳跃的代码。
我们需要在 BehaviorParameters 组件里面设置一下连续型操作个数和离散型操作个数,以及每个离散型操作的可能数.
案例中只有左右移动, 所以把连续型操作个数设置成 1 , 离散型操作个数设置成 0 .
4.5 加入训练结束条件和奖励机制奖励机制 - OnActionReceived(..)
在每一步结束后通过判断 AI 与球的距离来判断是否完成这次训练, 通过 SetReward(x)
方法来设置奖励值,
AI 会朝着奖励值最大的方向去训练自己; 然后用 EndEpisode()
结束这次训练; 最后进入初始化状态再次训练.
// Inside class MyFirstAgent
public override void OnActionReceived(ActionBuffers actions)
{
float moveDir = actions.ContinuousActions[0];
transform.position += Vector3.right * moveDir * moveSpeed * Time.deltaTime;
CalculateDistance();
}
private void CalculateDistance()
{
if (Vector3.Distance(largeGoal.position, transform.position) < 0.1f)
{
SetReward(1);
EndEpisode();
}
else if (Vector3.Distance(smallGoal.position, transform.position) < 0.1f)
{
SetReward(-1);
EndEpisode();
}
}
4.6 增加手动测试功能 - Heuristic(...)
通过重写 Heuristic(in ActionBuffers actionsOut)
方法, 来实现玩家可以通过自己的输入来控制AI; 这样能方便排查环境中是否有bug.
// Inside class MyFirstAgent
public override void Heuristic(in ActionBuffers actionsOut)
{
var continuousActions = actionsOut.ContinuousActions;
continuousActions[0] = Input.GetAxis("Horizontal");
}
4.7 在 Unity 中给写好的脚本赋值
把两个小球赋值给 LargeGoal 和 SmallGoal; 为了让AI更容易碰到小球加快训练速度, 我们先把距离和速度都设置成2.
4.8 添加 DecisionRequester 组件
在 BasicAgent 添加 DecisionRequester 组件, 用来为我们写好的 Agent 脚本申请决策.