ML Agents 学习笔记 (1)

发布时间 2023-07-04 14:05:00作者: JamesNULLiu

本文是对 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不断的向更大的球的方向移动.

ce5cb470a7909e670129691b49d9ab36.png
8d48b0b543463dff8f7982b4b7f4ba8e.png

2. 更改大小球位置

我们可以尝试编辑 AI 身上的 BasicController 脚本 (脚本位置在 \Assets\ML-Agents\Examples\Basic\Scripts 文件夹下),将里面的 k_SmallGoalPosition 的值改为 17,k_LargeGoalPosition 的值改为 7 (也就是将大球和小球之间的位置互换),再运行场景.

bad9772f8c75c8f54ea8d28c686536ab.png

可以看到AI并没有按照需求去朝着更大的球的方向移动,而是选择朝右边移动.

通过代码可以发现, 提供的 example AI 只知道自己的位置, 并不知道球的位置; 而因为训练时球的位置是固定的, 所以AI得出了 "一直向右走就能碰到大球" 的结论.
因此, 当环境改变时AI并没有改变移动方向.

3. 创建一个自己的环境

  1. 新建一个 Unity 项目, 不妨叫 To Large Goal.
  2. 打开项目, 执行以下步骤:
    • 上方菜单栏选择 Window
    • 选择 Package Manager
    • 弹出窗口中选择加号, 然后选择 Add Package From Disk
      edc9b24837e5ea24b76122bfd8073e23.png
    • 你在前面 clone 下来的 ML-Agents 项目 (后面统一叫原项目) 的一级目录中会有一个叫 "com.unity.ml-agents" 的文件夹, 文件夹里有一个叫 "package.json" 的文件. 上一步不是选择了 Add Package From Disk 吗, 就在弹出窗口将这个 "package.json" 添加到你自己的项目 To Large Goal 中.
      e4db634966c0211e0645474304cfc20d.png
  3. 将原项目中 Project\Assets\ML-Agents\Examples 文件夹下的 BasicSharedAssets 文件夹 (也就是上一节打开的 Basic 场景和共享的一些资源) 复制到新建项目 To Large Goal 中的 \Assets 文件夹下.
  4. 打开 To Large Goal, 先打开 Baisc Scene, 然后将 Scene 中原有的 Basic 对象从 Hierarchy 导航栏删除.
  5. 在 Project 导航栏中, 点开 \Assets\ML-Agents\Basic\Prefabs\Basic, 删除预设体中 BasicAgent 下已经添加的所有脚本.
  6. 在 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, smallGoalmoveSpeed 后面都会在 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知道 :

  1. 角色当前x轴坐标: transform.localPosition.x
  2. 大球现在的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 的组件) 里面设置一下观测数组的大小和需要在多少帧内连续观察.

076bd9186bc4ca098db2a21536ef1618.png

这里要注意, 如果 AddObservation() 方法里赋值的是三维向量, 就要算做观察了三次 (因为有x,y,z三个数据), 因此数组大小就要加三; 如果观察的是四元数的话数组大小就要加四.

例子中我们观察了两个 float 类型的值, 所以数组长度是2; 并且我们只需要观察大球当前的位置, 所以只需要观察 1 帧.

4.4 控制 AI 移动 - OnActionReceived(...)

重写 OnActionReceived(ActionBuffers actions) 函数, 这个函数用来指定每一步的AI的行为.

具体代码实现在下节和训练结束条件及奖励机制一起实现.

ML-Agents 支持两种类型的操作: 连续和离散.

  1. 可以通过 actions.ContinuousActions 数组来记录所有连续型的操作, 数组内的每一个值都在 [-1,1] 之间.
  2. 可以通过 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 .

6c155c01318c6604dc4547ca8ae29d16.png

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.

a42941f8421d41ff16bd870f4ac0d4d1.png

4.8 添加 DecisionRequester 组件

在 BasicAgent 添加 DecisionRequester 组件, 用来为我们写好的 Agent 脚本申请决策.

e7724f25447270e07c08e2d037d47d67.png