Unity ML-agents 参数设置解明
本文首发于:行者AI
Unity 是全球最受欢迎的游戏开发引擎之一,有大量的游戏开发者在使用 Unity 开发他们的游戏。在这个 AI、大数据等流行词遍布各行各业的时代,Unity 也没有被潮流抛下,推出了他们自己的基于深度强化学习来训练游戏 AI 的工具包 Unity ML-agents。这个工具包功能丰富,十分强大。可以帮助你在你的游戏内实现一个新的 AI 算法,并且快速的用到你的游戏当中。这么强大的工具包难以在一篇文章里面概括其所有功能。本文就先抛砖引玉,稍微讨论一下 Unity ML-agents 训练的时候需要用到的各种参数的意义,其常用的取值又是如何。
本文所有内容参考 Unity ML-agents 的官方文档(地址:https://github.com/Unity-Technologies/ml-agents/tree/main/docs)
1. 训练参数设置
在你开始你的训练之前,你需要针对你的训练任务设定一个训练参数文件(一般是一个.yaml 文件)。
接下来就简单介绍一下 ml-agents 环境里的参数设置概要。本文主要参考 ml-agents 最新版本关于参数设置的官方文档,做了一些概括性的翻译并加入一定个人理解。
具体文档地址:https://github.com/Unity-Technologies/ml-agents/blob/main/docs/Training-Configuration-File.md
训练参数主要分为常用训练参数(Common Trainer Configurations), 训练方式专用参数(Trainer-specific Configurations),超参数(hyper-parameters) ,奖励信号参数(Reward Signals), 行为克隆(Behavioral Cloning),使用 RNN 增强智能体记忆能力的参数(Memory-enhanced Agents using Recurrent Neural Networks),以及自我对抗训练参数(Self-Play)这几个大的模块。这几个模块下面又有一些小的模块,在后文会进一步说明,而且这些模块并不需要总是全都去设定。事实上,除了前三个模块是几乎每个环境训练必须的参数之外,其他的模块仅用在需要使用到对应功能的训练任务。接下来具体说明每个参数的含义。
2. 常用训练参数模块(Common Trainer Configurations)
trainer_type: (default =
ppo
) 这个参数决定了使用什么智能体训练算法,现在暂时只支持 Proximal Policy Gradient(PPO,具体应为 OpenAI 版本的 PPO2),Soft Actor-Critic(SAC),以及 MA-POCA。前两种都只是单智能体训练算法。 注意:在你改动了训练算法后,记得去调整一下后面的相应参数。对于不同的算法,下面的参数也往往有不同的适用范围,并非无缝衔接的。下面会具体说明。summary_freq: (default =
50000
) 这个参数决定了多少步数(step)之后,开始记录我们的训练统计数据。time_horizon: (default =
64
) 这个参数决定了在多少步数之后,开始把收集到的经验数据放入到经验池(experience buffer)。这个量同样也决定了使用多少步后的采样来对当前动作的预期奖励进行训练。简单来说,这个值如果越大,就相当于你更接近于一局(episode)游戏的真实回报,从而偏差更小。但是由于要进行一局游戏才能更新一个动作的奖励预期,这个过程相当的长,并且每局游戏可能情况变化很大。不同局之间,做同样的动作可能最终收益大相径庭(因为这个动作可能其实对这个游戏的影响根本没有那么大),从而导致方差较大。反过来,当你采样的步数太小,可能对最终的奖励预估会偏差很大,但是可能带来较小的方差。其实这也跟机器学习里面经典的简单模型复杂模型(过拟合欠拟合)问题一样,需要在方差和偏差当中取一个平衡。官方建议当你的环境太大(跑一步耗时太长)或者你设置的奖励比较密集的时候,可以把这个值设的低一点,反之则需要增大。比如在足球比赛这样奖励非常稀疏的任务当中,范例文档设置的该参数值为 1000。 注意,这个参数决定了采样的步数,和 batch_size、 buffer_size、 epoch 等参数亦有联系。后面提到这些参数的时候会再对其中关系加以说明。常见范围:32 - 2048max_steps: (default =
500000
) 这个参数决定了本次训练任务总共会进行多少步数。如果你有多个同样动作的智能体,他们每个的步数都会计入到这个总步数当中;同样的,如果你有多个环境并行的在多台服务器上运行,所有环境里的智能体的总步数都会计入考虑。所以,当你并行训练多 agent 算法的时候,务必将这个值设的更大一些。常见范围:5e5 - 1e7keep_checkpoints: (default =
5
) 这个参数决定了保留多少个训练时候产生的 checkpoint,其中每一个 checkpoint 都是在 checkpoint_interval 步数后产生的。checkpoint_interval: (default =
500000
) 如前文所说,这个参数决定了每多少步数以后,你的模型会存储一个节点。init_path: (default=None) 这个参数决定了你的模型是否从某个之前训练好的模型存储点继续训练。需要提供模型的确切位置,比如说,
./models/{run-id}/{behavior_name}
。其实在训练的时候,使用--initialize-from 这个 CLI 参数就足以让所有的训练任务从同一个存储模型继续训练,提供这个参数是为了方便让不同的训练从不同的模型继续训练(需要逐个设定)。很少用到threaded:(default =
false
) 开启 python 的多线程功能来实现一边训练一边存储模型,避免 I/O 消耗太多时间。官方建议在开启 self-play 功能的时候尽量避免使用该功能。
3. 接下来是一些常见超参数(hyper-parameters)的详细设定
hyperparameters → learning_rate: (default =
3e-4
),这个参数决定了梯度下降的学习率。当该值过大时训练会变的不稳定(reward 不能稳步上升,时常大幅震荡)。常见范围:1e-5 - 1e-3hyperparameters → batch size:这个参数决定了每次更新参数的时候,会有多少步的(state,action, reward...) 状态元组被用来进行学习。注意:这个参数应该永远是 buffer_size 的几分之一(因为实际上程序会先将 buffersize 拿来切分成 batchsize 大小的几个 batches。所以 buffer_size 永远要能被 batch_size 整除)。 对于一个连续动作空间的训练任务,这个参数应该设定在 1000 以上这个级别(因为动作空间需要尽可能多的采样来得到不同的数据),如果是离散空间,这个值往往设定在几十到几百就好了(根据你动作空间的大小来定)。常见范围:(连续-PPO)512 - 5120;(连续-SAC)128 - 1024;(离散,PPO&SAC):32 - 512.
hyperparameters → buffer size: (default =
10240
for PPO and50000
for SAC) 。 PPO:对于 PPO 算法来说,程序先要集满 buffer_size 那么多步数之后才会开启一轮训练。然后如前文所说,每次收集够了 buffer_size 个状态元组之后,我们把 buffer 里面的状态分成 buffer/batch_size 个 batch,然后每个 batch 进行一次训练更新参数, 这个过程重复 epoch(epoch 也是一个超参数)次。所以实际上每过一个 buffer_size 的采样之后,参数都会进行 num_epoch * buffer/batch 次数的更新。通常一个较大的 buffer_size 会带来一个更加稳定的训练结果。 SAC:对于 SAC 算法来说,通常来说经验池要是其设 episode 长度的几千倍,从而能让它训练的时候同时利用到较旧和最近的采样。常见范围:(PPO)2048 - 409600;(SAC) 50000 - 1000000hyperparameters → learning_rate_schedule: (default =
linear
for PPO andconstant
for SAC) 这个参数决定了是否使用学习率衰减的办法来稳定训练。两个可选的模式分别是 linear 和 constant,前者会对你的学习率进行线性递减,后者则不会改变。通常来说,学习率越大,往往容易出现训练不稳定,学习率越小则容易出现长时间训练不收敛的情况。所以使用学习率衰减先用较大的学习率快速的找到一个极值点方向,然后再逐步收缩学习率使得训练稳定在极值附近,而不会来回摆动。官方建议,对 PPO 开启 linear 模式,进行线性递减,以达到更快的收敛速度。对 SAC 来说,则建议维持学习率不变。
4. 接下来介绍一些常用的网络模型的超参数
twork_settings → hidden_units:(default = 1
28
) 这个参数决定了你的训练网络隐藏层的神经元数量,或者说它的维度。这个数值的大小决定了神经网络对游戏状态的表达能力。简单来说,较大的隐藏层往往对更大的观察空间有着较好的表达能力。所以这个值应该随着观察空间的维度变大而变大。常见范围:32 - 512network_settings → num_layers: (default =
2
) 这个参数决定了你的训练网络的层数。这个数字越大,你的模型越深。虽然多层叠加能够提高模型对环境的表达能力。但是模型并不是越深越好,过深的层数往往会带来梯度消失的问题,并且会使得训练速度变缓。建议优先加大 hiddent_unitys 再加大本参数。常见范围:1 - 3network_settings → normalize: (default =
false
) 这个参数决定了模型是否对输入的观察向量进行归一化(normalization)。具体公式大概为 (obs - mean) / std,其中 mean 和 std 分别是 observation 的平均值和标准差。官方建议,对于复杂的连续动作空间任务,使用 normalization 可能有帮助,但是对于简单的离散任务使用 normalization 则可能带来负作用。network_settings → vis_encode_type: (default = simple) 这个参数决定了你的编码器的结构。注意,ppo 也好,sac 也好,只是一个训练架构,不同的任务你使用 ppo 也需要不同的编码器。比如说一个卡牌类游戏,你可以把所有的观察向量用普通的全连接层处理,但是如果是一个动作游戏,你的观察可能是一张图片,那么这个时候你最好使用 CNN 作为你的特征提取编码器。这个参数就是在让你选择使用何种编码器。simple 是一个两层的卷积网络;Nature CNN 则是使用 Human Level Reinforcement Learning 这篇文章的实现**;resent 则是使用 IMPALA RESNET 这种架构**,这个网络的特点在于提取 pattern 能力强,不易梯度消失。match3 则是更适合于卡牌游戏的一种 CNN 结构,特点是更小的结构但是可以捕捉空洞空间的表示信息。fully_connected 则是一个单层全连接层。 因为卷积核大小的匹配要求,每种编码方式有一个最小输入维度的下限。比如 simple 最小 20 * 20;nature_cnn 最小支持 36 * 36;resent 最小支持 15 *15,match3 最小支持 5 * 5, 注意:使用 match3 来处理超大的输入维度可能会拖慢训练速度。
network_settings → contioning_type: (default = hyper) 这个参数决定了是否使用 hypernetwork 来处理任务目标的信息。hyper 网络的参数比较多,当使用 hyper 网络的时候请减少隐藏网络的层数。
5. Trainer-specific Configurations
接下来我们介绍一下针对不同训练算法的专门参数。
5.1 PPO-specific Configurations (PPO 专门参数)
beta: (default = 5.0e-3) 这个参数决定了鼓励探索多样化策略的熵正则项的强度(entropy regularization)。简单来说,熵正则化通常是为了丰富我们的策略函数探索不同策略和动作,我们使用一个 KL 散度去衡量新的策略和老策略的相似性,并通过增加 entropy 来让动作策略更加随机。换句话说,当 beta 越大,越鼓励探索。调节这个参数要结合 TensorBoard 里面的 entropy 和 reward 来看。当你的 entropy 下降的时候,reward 仍然没有起色,那就把这个 beta 增大使得 entropy 能够再持续一段时间。常见范围:1e-4 - 1e-2
epsilon: (default = 0.2) 这个参数决定了策略更新的速度。这个参数就是 ppo2 论文里面的 clip 范围,这个范围限制了每一次参数更新偏离范围,以保证 ppo 能够依靠 importance sampling 作为一个 on policy 的方法继续训练。 可以发现当 epsilon 越大,我们的旧策略参数 theta’变动较小。这可以使得训练更加稳定,但是随之而来的就是更慢的收敛速度。常见范围:0.1 - 0.3
lambd: (default = 0.95) 这个参数根据官方文档说明是决定了我们的算法多大程度上依赖预估的奖励值,又多大程度上依赖实际的奖励值。实际上这个 laambda 指的是 GAE 方法中的一个超参数 lambda。GAE 方法是在策略梯度下降中常用的一种 Advantage,用于更新控制 policy 在 gradient 方向上的更新幅度。GAE 方法简单来说就是求 TD(0), TD(1) 一直到 TD 无穷(即蒙特卡洛采样)的一个加权平均值。lambda 这个值介于 0 到 1 之间,当 lambda 等于 0 时就是 TD(0)估计,当 lambda 等于 1 时就是蒙特卡洛采样。常见范围:0.9 - 0.95
num_epoch:(default = 3) 这个参数决定了每次进行梯度下降更新的次数。详见前文 buffersize 的部分。常见范围:3 - 10
6. Reward Signals
奖励信号可分为两种,一种是外在的(extrinsic)来自于环境的奖励,一种是内在的(intrinsic)来自于内部的奖励(比如好奇心奖励等)。不管是内部还是外部奖励信号,都至少要设定两个参数,一个是信号强度(strength),一个是信号衰减率(gamma)。并且,你需要至少设定一种奖励,不然没办法训练。
6.1 Extrinsic Rewards
extrinsic → strength: (default = 1.0) 这个参数决定了模型收到的外部环境奖励信号的强度。 常见范围:1.00
extrinsic → gamma:(default = 0.99) 这个参数决定了远期奖励的衰减率。简单来说,加入我们的模型在某一步收到了 100 的奖励,那么我们前一步的奖励应该是多少呢?如果上一步没有得到其他的奖励,那么上一步我们的收益就应该是 gamma * 100 = 99, 同理,再上一步的收益就是 gamma^2 * 100, 以此类推。直观的理解,gamma 约接近一,那么较后期的收益也能反馈到前期的动作。反之就是动作策略进行学习的时候会更加倚重于短期内的回报。对于那种奖励较为稀疏,必须通过一系列动作之后才能获得一次奖励的任务,务必将这个值设定得更接近于一,反之则可以稍微小一些。常见范围:0.8 - 0.995
6.2 Intrinsic Reward
curiosity → strength: (default = 1.0) 这个参数决定了好奇心奖励的强度。这个比例需要调整到一个刚好的程度,使得好奇心奖励既不会淹没了外部奖励的信号,又不会和外部奖励比起来过于不值一提。常见范围:0.001 - 0.1
curiosity → gamma: (default = 0.99)如前所述,这个参数决定了远期奖励的衰减率。详见上文。
curiosity → network_settings: 这个参数主要是用来决定 ICM 模型的隐藏层维度的。即不能太大也不能太小。(64 - 256)
curiosity → learning_rate: (default = 0.99) 这个参数决定了你的 ICM 模型的学习率。过大的学习率会导致训练不稳定,过小的学习率会导致收敛缓慢。常见范围:1e-5 - 1e-3
7. Memory-enhanced Agents Using Recurrent Neural Networks
7.1 可以通过增加记忆模块的办法来增加模型的表达能力。注意,memory section 要加在 network 下面
network_settings → memory → memory_size: (default=128) 这个参数决定了你的 LSTM 网络的隐藏层的维度。这个值必须是偶数。大小视你的状态的复杂程度而定。最好要能够足够大,才能学习到如何记忆之前的情况。常见范围:32 - 256
network_settings → memory → sequence_length: (default = 64) 这个参数决定了你的记忆网络 RNN 循环的次数或者说是序列长度。为了能够训练这么长的序列,我们的采集到的经验也需要有这么长。所以根据我的猜测,尽管文档没有明说,但是这个参数一定要小于 time_horizon 的值。另外,这个数如果设定的太小,那么可能他无法记住太多的东西,反过来,如果太大,训练会很慢。
7.2 使用记忆网络需要注意以下几点
LSTM 网络在连续动作空间任务上表现不佳,官方建议更多的用在离散动作空间的任务上面。
添加了一个 RNN 层会增加神经网络的复杂度,务必适度降低神经网络的层数。
一定要记住把 memory_size 设定为偶数。
8. Self Play 参数设置
self play 这个 section 只有在对抗性训练的时候需要使用,如果仅仅只有一个 agent,或者多个 agent 中没有任何意义上的交互,则不需要设定这一个参数。Unity ml-agent 也是利用 self play 参数的加入来启动它自带的对抗性训练模块。
trainer_steps 和 ghost steps : 在理解 self play 参数之前我们需要先理解两个概念,trainer_steps 和 ghost_steps。在一个对抗训练当中,我们往往需要固定住一些 agent 的参数,在一定的步骤里面让他们作为对手去训练我们的 agents。那么我们的 learning agents 进行的步数就是 trainer_steps,而与之相对的,那些固定参数的对手所走的步数就是 ghost_steps,为什么这两个值要分别计数呢?因为有些游戏并不是对称对抗训练(asymmetrical game),比如我们训练一个 2v1 的场景,这时候在学习的队伍是 2 个 agent,对手可能就是 1 个 agent。在这种情况下,trainer_steps 的增长就会两倍快于 ghost_steps,因为我们计步的时候都是计算总和。理解了这两个概念之后,再来看下面的参数设定就会清楚很多,不然会一头雾水。
save_steps: 这个值决定了我们的 learning agents,每 save_steps 个 trainer steps 会去存储一个当前策略的参数。另外,如果 save_steps 足够大,比如我们把刚才例子里的 save_steps 改成 20480,那么在存储一次快照之前,参数就要进行至少 80 次更新,这样每个快照之间的难度曲线就会更陡峭,使得每次训练的不稳定性增大,但是带来的可能是更好的最终结果,以及在复杂任务上更好的表现。
team_change 和 swap_steps:刚才我们已经讨论过了什么是 trainer steps,什么是 ghost steps。现在来看一下这两个值怎么在对抗训练中决定我们更换对手的频率。team_change 参数是决定我们要用同一个 learning agent 训练多少次的参数。比如我们现在有红蓝两队球队,如果我们设定 team_change=10000,那红队就会先训练 10000 个 trainer steps 才会轮到蓝队。而 swap_steps 则决定了我们在一次 team change 之中,要更换几次对手。用上面的例子来看,就是红队在这 10000 个 trainer steps 里面要面对几个不同的蓝队的过去的快照。这里有一个简单的公式可以计算这种关系,假设我们现在有设定 team change 为 t,然后想要切换对手的次数为 x,而我们的队伍有 a1 个 agents,对手的队伍有 a2 个 agents,则我们的 swap_steps = (a2/a1) * (t / x), 如果这是一个对称对抗游戏,则上式可以简化为 t/x。 可以看见,team_changes 和 swap_steps,共同决定了一个更换对手的频率,这个频率越大,如同我们前面分析过的那样,我们可能会遇到更多不同等级和策略的对手,从而会学到更多东西,但也造成了训练不稳定,参数难以收敛的问题。但是每个 learning agent 学到的策略可能更加不局限于某种对手,更加通用,不会过拟合。
play_against_latest_model_ratio: (default =
0.5
) 这个参数决定了你和自己当前的模型对决的概率。这个值越大约容易和当前的自己的策略对决,也就更少的和自己以往的 snapshot 交战。
版权声明: 本文为 InfoQ 作者【行者AI】的原创文章。
原文链接:【http://xie.infoq.cn/article/8495f05cd02f9b23a04abce6d】。
本文遵守【CC BY-NC】协议,转载请保留原文出处及本版权声明。
评论