本文首发于:行者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 - 2048 -
max_steps: (default =
500000
) 这个参数决定了本次训练任务总共会进行多少步数。如果你有多个同样动作的智能体,他们每个的步数都会计入到这个总步数当中;同样的,如果你有多个环境并行的在多台服务器上运行,所有环境里的智能体的总步数都会计入考虑。所以,当你并行训练多agent算法的时候,务必将这个值设的更大一些。常见范围:5e5 - 1e7 -
keep_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-3 -
hyperparameters → 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 - 1000000 -
hyperparameters → learning_rate_schedule: (default =
linear
for PPO andconstant
for SAC) 这个参数决定了是否使用学习率衰减的办法来稳定训练。两个可选的模式分别是linear和constant,前者会对你的学习率进行线性递减,后者则不会改变。通常来说,学习率越大,往往容易出现训练不稳定,学习率越小则容易出现长时间训练不收敛的情况。所以使用学习率衰减先用较大的学习率快速的找到一个极值点方向,然后再逐步收缩学习率使得训练稳定在极值附近,而不会来回摆动。官方建议,对PPO开启linear模式,进行线性递减,以达到更快的收敛速度。对SAC来说,则建议维持学习率不变。
4. 接下来介绍一些常用的网络模型的超参数
-
twork_settings → hidden_units:(default = 1
28
) 这个参数决定了你的训练网络隐藏层的神经元数量,或者说它的维度。这个数值的大小决定了神经网络对游戏状态的表达能力。简单来说,较大的隐藏层往往对更大的观察空间有着较好的表达能力。所以这个值应该随着观察空间的维度变大而变大。常见范围:32 - 512 -
network_settings → num_layers: (default =
2
) 这个参数决定了你的训练网络的层数。这个数字越大,你的模型越深。虽然多层叠加能够提高模型对环境的表达能力。但是模型并不是越深越好,过深的层数往往会带来梯度消失的问题,并且会使得训练速度变缓。建议优先加大hiddent_unitys再加大本参数。常见范围:1 - 3 -
network_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交战。
behaviors:
SoccerTwos: #游戏的名字
trainer_type: ppo #选定使用何种训练算法
hyperparameters: #PPO算法的超参数设置
batch_size: 2048
buffer_size: 20480 # buffer大小必须是batch大小的整数倍,这里是十倍
learning_rate: 0.0003
beta: 0.005 #熵正则的超参数
epsilon: 0.2 #PPO算法的clip范围
lambd: 0.95 #GAE算法的lambda
num_epoch: 8 #每次更新训练多少次
learning_rate_schedule: linear #学习率衰减方式,这里是线性衰减
network_settings: #处理observation信息的网络结构设置
normalize: false
hidden_units: 256
num_layers: 2
vis_encode_type: simple
reward_signals: #奖励超参数设置
extrinsic:
gamma: 0.99
strength: 1.0
keep_checkpoints: 5 #一共保留最近的五个checkpoint
checkpoint_interval: 100000 #每100000个timestep保存一个checkpoint
max_steps: 50000000 #最多训练这么多不(注意,这是多个agent加起来的值)
time_horizon: 1000
summary_freq: 5000
threaded: false # 是否使用线程,在使用self-play功能时最好关掉
self_play: #self-play相关参数设定
save_steps: 50000
team_change: 250000
swap_steps: 2000
window: 10 #一共保留十个过去过去的snapshot
play_against_latest_model_ratio: 0.5
initial_elo: 1200.0
PS:更多技术干货,快关注【公众号 | xingzhe_ai】,与行者一起讨论吧!