基于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 = &#39;UnityEnvironment&#39;
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是能够做到次次满分方差为零的。我想应该是我训练出的动作分布还是有一定概率采样出很差的动作,虽然正态分布本就不会有动作是概率为零的,但这需要多个连续时间步都采样到了很差的动作才能失败。可能陷入局部最优了?这一点我没法调试成最优的情况了,我最后的训练效果如下:
谢谢,给出了指导性方法
页:
[1]