Ylisar 发表于 2022-8-30 20:23

基于ML-Agents和gym_unity用PPO实现Unity环境的3DBall ...

这是ML-Agents的官方案例环境,游戏的目标就是旋转小方块使得小球保持在头顶不掉落,像是3D版的CartPole。
本文将用基于ML-Agents的框架和基于gym_unity使用自己的本地训练代码两种方式分别训练。


环境状态、智能体动作、奖励设计

这些都是官方预设的,我暂时没有去动它,下面来解释一下。
环境状态:

在Agent脚本中(c#),用一个类来采集observations,分别有:
方块在x和z轴上的旋转角度(2d)
方块和小球的相对位置(3d)
小球在各方向上的速度(3d)
因此observations有8维
public override void CollectObservations(VectorSensor sensor)
{
    if (useVecObs)
    {
      sensor.AddObservation(gameObject.transform.rotation.z);
      sensor.AddObservation(gameObject.transform.rotation.x);
      sensor.AddObservation(ball.transform.position - gameObject.transform.position);
      sensor.AddObservation(m_BallRb.velocity);
    }
}
智能体动作:

从网络返回的动作是二维的,分别是方块在x和z轴上的需要旋转的角度
var actionZ = 2f * Mathf.Clamp(actionBuffers.ContinuousActions, -1f, 1f);
var actionX = 2f * Mathf.Clamp(actionBuffers.ContinuousActions, -1f, 1f);

if ((gameObject.transform.rotation.z < 0.25f && actionZ > 0f) ||
        (gameObject.transform.rotation.z > -0.25f && actionZ < 0f))
{
        gameObject.transform.Rotate(new Vector3(0, 0, 1), actionZ);
}

if ((gameObject.transform.rotation.x < 0.25f && actionX > 0f) ||
        (gameObject.transform.rotation.x > -0.25f && actionX < 0f))
{
        gameObject.transform.Rotate(new Vector3(1, 0, 0), actionX);
}在收到动作的二维数组后,Agent脚本又做了几步处理:
一是做了一个clip,使得旋转角度控制在[-2,2]之间;
二是让x和z轴在旋转角度过大的状态时,只有往平稳状态方向执行旋转才执行旋转。
这样做是为了不至于倾斜角度已经过大时,再旋转到倾斜角度更大的状态。
奖励设计:

在脚本中设置每过一个时间点,奖励+0.1,小球掉落就奖励-1并结束episode。
并在Agent面板设置Max Step为5000,达最大步长时结束episode。
if ((ball.transform.position.y - gameObject.transform.position.y) < -2f ||
        Mathf.Abs(ball.transform.position.x - gameObject.transform.position.x) > 3f ||
        Mathf.Abs(ball.transform.position.z - gameObject.transform.position.z) > 3f)
{
        SetReward(-1f);
        EndEpisode();
}
else
{
        SetReward(0.1f);
}
用ML-Agents框架训练

yaml配置文件,用以初始化网络参数:
behaviors:
3DBall:
    trainer_type: ppo
    hyperparameters:
      batch_size: 64
      buffer_size: 12000
      learning_rate: 0.0003
      beta: 0.001
      epsilon: 0.2
      lambd: 0.99
      num_epoch: 3
      learning_rate_schedule: linear
    network_settings:
      normalize: true
      hidden_units: 128
      num_layers: 2
      vis_encode_type: simple
    reward_signals:
      extrinsic:
      gamma: 0.99
      strength: 1.0
    keep_checkpoints: 5
    max_steps: 500000
    time_horizon: 1000
    summary_freq: 12000命令行输入命令使用mlagents框架训练:
mlagents-learn 3DBall_ppo.yaml


训练完生成的3DBall.onnx文件可在Unity3D中放在Behavior Parameters的Model中,作为控制Agent的网络参数。
到这基于ML-Agents框架的训练就结束了,但我还想跑一下自己的算法,于是在网上搜索到了用gym_unity来实现。

使用gym_unity接自己的算法训练

包装过程与限制

先将unity环境build出来生成exe文件,在build的时候可以设置Agent的Time Scale为20,就是把Agent的执行速度提升20倍,加快与环境的交互。
但是就我了解和探索到的,使用gym_unity只能有一个Agent在环境当中,所以多智能体和复制多个Agent加快训练都是不行的。
用以下代码块,导入所需包,并将UnityEnvironment.exe包装成gym的env,之后就跟gym一样可以调用reset()等方法。
from mlagents_envs.environment import UnityEnvironment
from gym_unity.envs import UnityToGymWrapper

env_directory = 'UnityEnvironment'
unity_env = UnityEnvironment(env_directory, base_port=5005, no_graphics=True)
env = UnityToGymWrapper(unity_env, uint8_visual=True)no_graphics参数控制在训练时是否显示互动窗口
PPO算法设计

首先初始化Actor和Critic两个网络。
Actor网络用于输入状态,输出动作的分布。由于是连续动作空间,需要一个概率分布函数,我看大家一般都是用正态分布来替代的,为了确定这一分布,需要给定均值和方差。
我一开始是用网络输出 action_dim * 2 维的张量,分别用于均值和方差,发现效果不好,然后借鉴了别人的做法,网络只输出均值,然后用Tanh把均值控制在[-1,1]之间,方差用全零张量初始化另外用参数训练,然后用指数函数控制方差大于零。Actor类如下:
class ActorNetwork(nn.Module):
    def __init__(self, action_dim, input_dims, alpha, fc1_dims=128, fc2_dims=128):
      super(ActorNetwork, self).__init__()

      self.actor = nn.Sequential(
            nn.Linear(in_features=input_dims, out_features=fc1_dims),
            nn.ReLU(),
            nn.Linear(fc1_dims, fc2_dims),
            nn.ReLU(),
            nn.Linear(fc2_dims, action_dim)
      )

      self.log_std = nn.Parameter(T.zeros(action_dim))

      self.alpha = alpha
      self.optimizer = optim.Adam(self.parameters(), lr=self.alpha)

    def forward(self, state):
      mean = T.tanh(self.actor(state))
      std = T.exp(self.log_std.expand_as(mean))
      dist = Normal(mean, std)

      return distCritic网络用于输入状态,输出价值。没什么特别的。Critic类如下:
class CriticNetwork(nn.Module):
    def __init__(self, input_dims, alpha, fc1_dims=128, fc2_dims=128):
      super(CriticNetwork, self).__init__()

      self.critic = nn.Sequential(
            nn.Linear(input_dims, fc1_dims),
            nn.ReLU(),
            nn.Linear(fc1_dims, fc2_dims),
            nn.ReLU(),
            nn.Linear(fc2_dims, 1)
      )

      self.alpha = alpha
      self.optimizer = optim.Adam(self.parameters(), lr=self.alpha)

    def forward(self, state):
      value = self.critic(state)

      return value然后写个Agent类。首先需要采样出action,用于和环境交互,再算出采样这个动作的概率。这里有个细节,由于动作是二维的,需要把这两个维度的概率相乘当作这个动作的概率,这里就因此对它们的log求和,把这个概率同action一起需要添加到用于训练的Memory里。选择和环境交互的动作的方法如下:
def choose_action(self, observation):
    state = T.tensor(observation, dtype=T.float).to(self.actor.device)

    dist = self.actor(state)
    action = dist.sample()

    log_probs = dist.log_prob(action)
    log_probs = log_probs.sum(-1, keepdim=True).detach().numpy()
    action = action.detach().numpy()

    return action, log_probs这样就足以和环境互动了,然后把采集到的状态、动作、概率、奖励、下一个状态、是否完成一次轨迹等等信息传到Memory中。
收集到了一批数据之后,就可以计算Loss开始学习网络了。
先计算GAE,据此计算出TD error,得到critic_loss,再计算出batch中的action得到新的log_probs,根据PPO2的公式计算出actor_loss。
我看了一下MLAgents官方的PPO源码,是用actor_loss、critic_loss和分布的熵的加权求和作为整个Loss,并且还给熵的系数做了线性递减,我没有做递减,不过我试了一下这三者的系数变化确实会影响整个训练效果。这里也可以对两个loss分别用梯度更新,不算上熵,或者把熵放到actor_loss里,也是有效果的。
我的训练代码如下:
def learn(self, alpha):
    for _ in range(self.n_epochs):
      state_arr, action_arr, old_prob_arr, state__arr, reward_arr, dones_arr, batches = \
            self.memory.generate_batches()

      with T.no_grad():
            dones_arr = dones_arr.astype(int)
            v = self.critic(T.tensor(state_arr)).flatten()
            v_ = self.critic(T.tensor(state__arr)).flatten()
            deltas = T.tensor(reward_arr) + self.gamma * T.mul(v_, (T.ones(len(dones_arr)) - T.tensor(dones_arr))) - v
            deltas = deltas.flatten().numpy().tolist()
            gae = 0
            discount = 1
            advantage = []
            for index in range(1, len(deltas)+1):
                delta = deltas[-index]
                discount = 1 if dones_arr[-index] == 1 else discount
                gae *= (1 - dones_arr[-index]) * discount
                gae += delta
                discount *= self.gamma * self.gae_lambda
                advantage.insert(0, gae)
      advantage = T.tensor(advantage).to(self.actor.device)
      values = T.tensor(v).to(self.actor.device)

      for batch in batches:
            states = T.tensor(state_arr, dtype=T.float).to(self.actor.device)
            old_probs = T.tensor(old_prob_arr).to(self.actor.device)
            actions = T.tensor(action_arr).to(self.actor.device)

            new_dist = self.actor(states)
            critic_value = self.critic(states)
            critic_value = T.squeeze(critic_value)

            new_probs = new_dist.log_prob(actions)
            new_probs = new_probs.sum(1, keepdim=True)
            prob_ratio = new_probs.exp() / old_probs.exp()
            dist_entropy = new_dist.entropy().sum(1, keepdim=True)
            surr1 = advantage * prob_ratio
            surr2 = T.clamp(prob_ratio, 1 - self.policy_clip, 1 + self.policy_clip) * advantage
            actor_loss = (-T.min(surr1, surr2)).mean()

            returns = advantage + values
            critic_loss = ((returns - critic_value) ** 2).mean()

            entropy_loss = - dist_entropy.mean()

            out_actor_loss = actor_loss
            out_critic_loss = critic_loss
            out_entropy = - entropy_loss

            total_loss = actor_loss + 0.5 * critic_loss + 0.00001 * entropy_loss
            self.actor.optimizer.zero_grad()
            self.critic.optimizer.zero_grad()
            total_loss.backward()
            self.actor.alpha = alpha
            self.critic.alpha = alpha
            self.actor.optimizer.step()
            self.critic.optimizer.step()

    self.memory.clear_memory()

    return out_actor_loss, out_critic_loss, out_entropy, total_loss训练过程感悟和待优化的部分

一开始我固定了学习率为0.0003,训练一段时间后平均回报总是开始震荡地很厉害,训练过程不那么平稳。然后我根据MLAgents的PPO源码按照保留一个小数字,学习率按训练的进度线性递减来设定,这样训练过程特别平稳。
alpha_ = alpha * (1 - n_steps / max_step) + 1e-10和环境互动了多少步拿去做一次训练,这个也很关键,调了一些最后还是用的MLAgents的参数。
我也逛了一下别人是怎么做PPO的,有人说让Actor和Critic共享几层网络能提高鲁棒性。这个我没有尝试。
我最后训练出的一个曲线,还是没有达到必定得满分的情况,尽管十几次里面可能只有一次不是满分,但是我用MLAgents的PPO是能够做到次次满分方差为零的。我想应该是我训练出的动作分布还是有一定概率采样出很差的动作,虽然正态分布本就不会有动作是概率为零的,但这需要多个连续时间步都采样到了很差的动作才能失败。可能陷入局部最优了?这一点我没法调试成最优的情况了,我最后的训练效果如下:

Mecanim 发表于 2022-8-30 20:30

谢谢,给出了指导性方法
页: [1]
查看完整版本: 基于ML-Agents和gym_unity用PPO实现Unity环境的3DBall ...