OpenAI Five 论文学习
1 主要挑战和简化
1.1 挑战
超长决策链
每局游戏大约需要 \(20000\) 个 \(step\) (决策步),相比之下,国际象棋一般在 \(80\) 步,围棋在 \(150\) 步左右。\(Dota2\) 的帧率是 \(30/s\),而一局游戏的平均时长是 \(45\) 分钟,\(OpenAI\ Five\) 的设定是每 \(4\) 帧(一个timestep)做一个动作,所以总的决策数(动作数)为:
部分可观测
游戏中每方都只能看见他们的单位和建筑附近的区域状态(战争迷雾),因此要达到很高的强度,需要基于不完全数据进行推断,以及进行对手建模。
高维动作和观察空间
\(Dota2\) 的游戏环境较为复杂,双方共十个英雄,数十个建筑,小兵等,大概可以相当于每步可以观测到 \(16000\) 个不同的变量(包括连续和离散两种类型)来描述环境;同样的,在对动作空间进行离散化之后,在每步可供选择的动作在 \(8000\) 到 \(80000\) 不等(视英雄而定);同样的,象棋和围棋与此相比会简单很多,象棋是每步观测变量约为 \(1000\) 个,每步可选动作数量约为 \(35\) 个,而围棋是每步观测变量约为 \(6000\) 个,每步可选动作数量约为 \(250\) 个。
1.2 简化
采用游戏环境的限制
目前 \(OpenAI\ Five\) 为了适当降低系统的复杂度,其训练的 \(Dota2\) 环境与实际的游戏环境有如下区别:
● 游戏中有 \(117\) 个不同的英雄,在 \(Openai\ Five\) 里只支持其中的 \(17\) 个;
● 游戏中有部分道具允许玩家同时控制多个单位,在 \(Openai\ Five\) 中禁用;
● 某些游戏机制是由手写逻辑而不是策略控制的:如英雄购买物品和能力的顺序、独特信使单位的控制以及英雄保留哪些物品(实际上如果这些也都使用策略控制,在合理且充足的训练之后,大概率能达到比手写逻辑更好的效果)。
2 训练模型
如下图是简化的 \(OpenAI\ Five\) 模型架构:
首先将复杂的观察空间处理为单个向量,然后将向量输入进 \(LSTM\)(单层 \(4096\) 个单元),输出 \(action\) 和 \(value\),团队中的五个英雄使用相同的网络结构进行控制(由同一个网络复制而来,如上图中 \(LSTM\) 这部分复制了五份,并且 \(Tied\ Weights\)[共享参数]),它们具有几乎相同的输入,每个英雄都有自己的隐藏状态(小技巧: 观测中包含了很少量的衍生信息,如游戏中每个单元与自身的距离)。各个网络依据上图中的“\(Hero\ Embedding\)”来识别不同的英雄,从而输出不同的动作,所以上图中“\(Hero\ Embedding\)”这部分复制了五份。\(LSTM\) 构成了模型总参数的 \(84%\)。
\(Tied\ Weights\):可以理解为参数共享,这样在网络学习的过程中只需要学习一组权重,好处有两点,一是减少了参数的数量,加速训练过程,二是 \(Tied\ Weights\) 可以被看做是一种正则化形式,在实践中能获得更好的性能。
2.1 观测空间
在每个时间步中,每位英雄观察到大约16000维的输入,下图为观察空间的示意图:
其中蓝色带代表分类数据(\(categorical\)),黄色带代表连续或布尔数据。
在输入神经网络之前,所有的浮点观测值(包括被视为恰好取值为 \(0\) 或 \(1\) 的浮点数的布尔值)都进行标准化处理。对于每次观测,会对所有观测到的数据计算均值和标准差,在每个时间步上,减去均值并除以标准差,将最终结果裁剪为 \((-5,5)\) 之间。
如下为观测的完整清单:
其中:
● \(Nearby\ map\) 为
● \(Previous\ Sampled\ Action\) 为
● \(Per-modifier\) 为每个单位的状态栏信息(\(Buff\) 效果),每个英雄单位(共 \(10\) 个英雄单位)有 \(10\) 个状态,每个非英雄单位(共 \(179\) 个非英雄单位)有 \(2\) 个状态;
● \(Per-item\) 为
● \(Per-ability\) 为每个英雄单位的技能信息,每个英雄有 \(6\) 个技能;
● \(Per-pickup\) 为
● \(Minimap\) 为
前面提到,在观测到的状态空间上有约 \(16000\) 个变量,以上两图列举了变量设计,可以用以下的公式进行简要的汇总:
如果类别中可见单位的数量小于分配的数量,则其余部分将用零填充。如果更多,我们只观察最接近友方英雄的单位。战争迷雾中的单位不会被观察到。当敌方英雄处于战争迷雾中时,我们会重复使用上一个时间步(单位可见时)的观察结果。
2.2 动作空间
玩过 \(Dota2\) 或者类似游戏的人都知道,游戏的输入是通过键盘和鼠标来控制英雄实现的,包括了移动,攻击,释放技能,使用道具等,但这里的输入包括了许多在屏幕上点击拖拽的操作,涉及在空间和时间上进行连续变化的操作,如何合理的进行离散化是动作设计的主要难点。
\(OpenAI\ Five\)在动作上的处理是将各种动作抽象为一个带参数的函数形式,通过不同的函数类型配合不同的参数来模拟不同的动作,可以按照下式理解:
这里的 \(p\) 代表 \(primary\ action\),共 \(30\) 种,可以按照动作的目标,分为 \(6\) 类,类型决定了动作使用的参数:
这六种动作分别解释为:
● 无目标型:如使用道具,例如喝药,参数包含 \(Delay\);
● 点目标型:如移动,参数为 \(Delay\) 和 \(Offset\);
● 单位目标型:如攻击,参数为 \(Delay\) 和\(Unit\ Selection\);
● 单位偏移目标型:如某些指向型技能,例如火枪的榴霰弹,参数为 \(Delay\),¥Unit\ Selection$ 和 \(Offset\);
● 传送目标型:如使用回城卷轴,参数为 \(Delay\),\(Unit\ Selection(Teleport)\) 和 \(Offset\);
● 守卫目标型:如放置假眼,参数为 \(Delay\) 和 \(Offset(Ward)\)。
其中,三种动作参数分别解释为:
● \(Delay\):执行动作的延迟,\(0\) ~ \(3\) 的整数,用于控制对应动作的生效时间,\(0\) 对应当前帧,\(3\) 对应当前这个 \(timestep\) 的最后一帧(在\(OpenAI\ Five\)中,每四帧为一个 \(timestep\));
● \(Unit\ Selection\):所有可能的 \(189\) 个单位,进行选择,如果选定的单位无效,返回空动作;
● \(Offset\):一个二维的网格,大小为 \(9*9=81\),网格中心为施法者自己或者 \(Unit Selection\) 的对象。
在动作选择前会将对动作进行过滤,剔除因冷却或无有效目标等无法执行的动作,在每个时间步,都使用过滤器来限制可用操作的集合,并向模型提供最终选择的动作。模型还会选择动作参数,在每个时间步,模型都会为每个动作参数输出一个值;根据主要操作,其中一些被读取,另一些被忽略。在进行参数优化的时候(梯度下降法),我们会屏蔽掉被忽略的那些参数输出,因为它们的梯度将是纯噪声。这里有 \(3\) 个参数输出:延迟(\(4\) 种)、单位选择(\(189\) 种)和偏移(\(81\) 种)。
通过上述的设计,完整的动作空间应该包括 \(30 * 4 * 189 * 81 = 1837080\) 种组合,但实际上大部分情况下,由于动作或者动作目标的限制,许多动作是无效的。在实际的比赛中,平均来说,动作空间的大小在 \(8000\) ~ \(80000\) 的范围内(不同英雄动作空间差异较大)。
值得注意的是,在 \(Unit\ Selection\) 的处理上,传送和普通目标的 \(Unit Selection\) 进行了区分,这是由于传送的目标分布与普通目标不太一样,而且在训练样本中极为不平衡(出现传送的比例非常低),所以在动作中选择分开进行处理。
同样,“偏移”参数也被分为几种:
● 常规偏移(\(Regular\ Offest\));
● 施法者偏移(\(Caster\ Offest\)),对于仅对施法者有意义的动作,指向型技能;
● 守卫放置偏移(\(Ward\ Placement\ Offset\)),对于放置观察守卫的罕见动作。
脚本动作
还有一部分动作是基于脚本写死的,不属于 \(AI\) 控制部分。在项目开始阶段,大部分操作都是基于脚本进行控制的,随着训练的深入,逐渐将一些操作从脚本逻辑中删除并交给模型控制,每次这样做一方面提高了强化学习系统的能力上限,另一方面也增加训练的复杂度和探索的风险。
即使在逐步系统地添加这些新动作时,偶尔也会遇到不稳定的情况;例如,智能体可能很快就会学会永远不会采取新的行动(因此无法探索该行动对整体有帮助的那一小部分情况),因此无法学习游戏中哪些情况采取新动作会有所帮助。
最后,仍有一些操作是用脚本进行控制的(虽然理论上说将其开放由模型控制会进一步提高模型能力的上限,但是目前并没有这么做),这些由脚本控制的操作列举如下:
● 技能加点(\(Ability\ Builds\)):每个英雄都有四种可升级的技能。在目前的系统中,遵循固定的时间表进行技能加点(在第 \(1\) 级提高能力 \(X\),然后在第 \(2\) 级提高 \(Y\),然后在第 \(3\) 级提高 \(Z\),等等)。在训练中,我们围绕这个固定脚本进行一定程度的随机化,以确保模型对于选择不同加点顺序的对手来说是鲁棒的(因为实际的对手并不会按照这个规则进行技能加点,因为给一定的随机性,确保训练出的智能体不是只能应对这种加点顺序的对手)。
● 物品购买(\(Item\ Purchasing\)):对于消耗品,使用一个简单的逻辑来确保智能体始终拥有一组特定的消耗品;用完一件后,再购买一件新的。游戏达到一定时间后,将停止购买消耗品。对于非消耗品,使用类似于技能加点的策略,遵循固定的时间表(首先购买 \(X\),然后是 \(Y\),然后是 \(Z\),等等)。在训练时,再次随机扰乱这些构建,以确保对使用不同购买策略的对手的鲁棒性。
● 物品放置(\(Item\ Swap\)):每个玩家可以选择他们持有的 \(6\) 件物品保留在他们可以主动使用的“背包”中,在他们的“仓库”中留下最多 \(3\) 件不常用的物品。我们没有让模型控制这一点,而是使用启发式方法,大约将最有价值的物品保留在“背包”中。
● 信使控制(\(Courier\ Control\)):双方都有一个“信使”单位,它不能战斗,但可以将物品从商店运送给购买它们的玩家,使用基于状态机的逻辑来控制这个角色。
2.3 奖励设计
为了简化信用分配问题(找出智能体在游戏过程中采取的众多行动中哪些行动导致了最终的正奖励或负奖励的任务),使用了更详细的奖励函数,如下为根据人类玩家的经验给出奖励或惩罚:
其中,有些奖励(\(Solo\) 类型)是给到采取该动作的英雄,有些(\(Team\) 类型)是分配给全队的每个英雄。这意味着,当 \(Team\ Spirit\) (团队精神)为 \(1.0\) 时,\(Team\) 奖励的奖励总额是 \(Solo\) 奖励的五倍。\(Solo\) 奖励和 \(Team\) 奖励汇总如下:
● \(Solo\) 奖励:死亡、经验获得、获得金币、使用金币、血量变化、蓝量变化、击杀英雄、击中(英雄/小兵/建筑物/野怪)、走线;
● \(Team\) 奖励:比赛胜利、鸡死亡、基地血量变化、获得肉山盾、推1塔、推2塔、推3塔(高地塔)、推4塔(门牙塔)、推兵营、推圣坛、解锁超级兵。
对于上表中关于推塔的奖励(标注星号 $* $ 的),三分之二的奖励是随着塔失去生命值而线性获得的,三分之一是在塔推掉时时一次性获得的。
对于上表中关于生命值的奖励,设 \(x\) 为当前生命值的百分比,则计算奖励时被表示为:\((x+1-(1-x)^4)/2\),该函数是在项目初期拟定的,后续没有进行优化调整,该函数曲线形如下图(低生命值时更敏感):
在 \(reward\) 基础设计之外,还有三个重要的因素:
零和
游戏玩法本身是零和的,一方的胜利意味着另外一方的失败,因此折算到所有的 \(reward\) 上,都是零和的,通过从每个英雄在指定事件上获得的 \(reward\) 分数减去敌方全队在同样事件上的平均得分,来确保所有奖励都是零和的。
时间权重
游戏机制导致英雄的能力随着时间增长而增长,这就导致后期英雄更容易通过击杀小兵等获得大量 \(reward\),从而导致学习过程将完全集中在游戏的后期阶段,而忽略了早期阶段。具体的修正方法是简单的重新归一化:将除赢/输奖励外的所有奖励乘以在游戏过程中呈指数衰减的因子。奖励按照下式进行缩放(其中T为游戏从开始以来的时间):
团队精神
\(Dota2\) 是一个团队游戏,而建模的部分没有考虑到多智能体之间互动的问题,由于在一个队伍中有多个英雄,因此在信用分配问题上还有一个新的问题,需要了解五个英雄中的哪一个的行为带来了积极的结果。
因此引入了团队精神( \(\tau\) ),它衡量团队中有多少智能体分享了队友的战利品。
团队精神( \(\tau\) )用来控制在最后 \(reward\) 计算中自身产生的 \(reward\) 部分和团队产生 \(reward\) 部分的比例:
这里 \(r_i\) 表示每个英雄最终获得的 \(reward\),而 \(\rho_i\) 是其自己产生的 \(reward\),\(\bar{\rho}\) 是团队的 \(reward\) 的平均值。当 \(\tau = 0\) 时,没有团队合作,每个英雄都只考虑自己的操作带来的收益,而当 \(\tau = 1\) 时,就是大家都考虑团队的整体收益。
最终我们关心的是肯定是 \(\tau = 1\) 的优化,我们希望选择的行动能够优化整个团队的成功。然而,我们发现较低的团队精神会减少早期训练中的梯度方差(过早强调团队合作只会带来更多的随机性),确保智能体在提高其单独参与战斗的能力方面获得更明确的奖励(更有效的帮助智能体学习到单体操作的技巧)。
2.4 网络结构
\(Openai\ Five\) 使用五个复制的神经网络,每个神经网络负责团队中的一个英雄的观测和动作。该网络包括三个部分:首先处理观测量,将其汇总到单个向量中,然后由单层大型 \(LSTM\) 处理,然后对 \(LSTM\) 的输出做线性映射产生输出。
观测数据处理
由于所感知的环境比较复杂,包含了许多不同类型的输入,从数据结构上面来说有连续型的也有离散型的,即包括了角色状态相关的也包括了空间相关的,所以在输入进网络之前,重要的问题就是如何将这些变量拼接和向量化。
\(Openai\ Five\) 的具体做法如下图所示:
从图中可以看到,在 \(Openai\ Five\) 中,对不同类型的变量分别进行不同的处理:
● 连续型:仅作归一化,无学习处理;
● 类别型:做嵌入表示;
● 空间数据:主要是小地图和邻近环境,\(2\) 层卷积;
● 无序集合:譬如己方\敌方英雄、小兵、野怪等单位的状态变量,先经过 \(2\) 层全连接,然后分为两路:\(①\) 全集合表示,经过一层 \(max\ pooling\),用于表示该集合的平均状态;\(②\) 元素嵌入表示,直接用全连接的输出作为对应单位的嵌入表示。
对于无序集合(观测的一个共同特征),我们使用“集合处理”(\(Process\ Set\))模块。处理能力/物品/状态(\(Buff\))的集合处理模块中的权重在友方和敌方英雄之间共享;处理状态(\(Buff\))的集合处理模块中的权重在友方/敌方/中立非英雄单位之间共享。
除了主要的游戏状态观测(\(Game\ State\))之外,我们还从单元的集合处理(\(Process\ Set\))的嵌入输出(\(Embedding\ ouput\))数据中提取单元嵌入数据(\(Unit Embeddings\)),用于在下一步中使用。
这样处理完之后,得到一个全局表示和自己(一个网络副本)控制的英雄的状态的嵌入表示,为了告诉每个网络副本它控制的是团队中的哪个英雄,我们将从上图单元嵌入输出(\(Unit\ Embedding\ ouput\))中提取出的受控英雄的单元嵌入数据(\(Unit\ Embedding\))附加到“游戏状态”向量(\(Game\ State\))中。
可以发现,五个网络副本(五个英雄)中每一个的 \(LSTM\) 的输入几乎都是相同的(唯一的区别是附近的地图,先前的动作以及每个单元观测值的一小部分)。 为了允许每个副本在需要时响应其他副本的输入的不同部分,添加了 \(cross-hero\ pool\) 的操作(实现英雄之间的数据交互,如告诉队友自己学了什么技能,什么技能正在冷却,买了什么装备等信息),在该操作中,将五个网络的前 \(25%\) 的向量进行了\(max\ pooling\),具体如下图所示:
如上图所示,对于一个英雄来说,全局表示和自己状态的嵌入表示通过一个全连接层进行拼接,然后再经过一个 \(cross-hero\ pool\) 的操作来获取别的英雄的部分信息,然后输入到一个单层的 \(4096\) 单元的 \(LSTM\) 中。
输出动作获取
根据 \(LSTM\) 的输出和受控英雄的单元嵌入数据(\(Unit\ Embedding\))获取动作和动作参数,该部分的网络结构如下图所示:
从上图可以看出,\(LSTM\) 的输出分多个头,经过了一系列的映射才变成最后的输出,主要是以下三个步骤:
● \(LSTM\) 的输出层和可用动作交叉,基于动作分布采样生成选用的动作 \(id\);
● 基于选定的动作 \(id\),通过 \(attention\) 机制来选择可能的动作目标单位,这里需要用到在处理原始输入时产生的各个单位的嵌入表示(\(Unit\ Embedding\));
● \(LSTM\) 的输出经过全连接层,得到动作的偏移(\(Offest\))和延迟(\(Delay\))等输出变量。
可以看到,模型从设计上来说本身是具有泛化能力的。
另外,状态价值(\(value\))也是 \(LSTM\) 的输出进行线性映射得到的,所以价值函数和策略函数共享网络和梯度。
2.5 模型详解
3 训练机制
\(Openai\ Five\) 使用 \(PPO\) 来训练,优化算法使用 \(GAE\),一种基于优势的标准方差减少技术,可以稳定和加速训练。使用了一个中心化的共享式的 \(LSTM\) 模块来训练网络,这一模块会向不同的全连接层提供输入,由不同的全连接网络输出策略和 \(Value\)。
3.1 整体架构
整体训练系统的架构如下图所示:
从上图可以看出,训练系统主要由四个部分组成:\(Rollout\ Worker\)、\(Forward\ Pass\ GPU\)、\(Optimizer\ GPU\)、\(Controller\),下面分别介绍。
Rollout Worker
\(CPU\) 集群,负责运行 \(Dota2\) 游戏环境,将观测转换为数值形式的 \(observation\) 并且计算 \(reward\) 和 \(GAE\) 值。
这里以大约 \(0.5\) 倍的实时速度运行游戏,因为我们发现可以以此速度并行运行略多于两倍的游戏,从而提高总吞吐量。
\(51200\) 个 \(CPU\) 核上运行 \(57600\) 个 \(Rollout\ Worker\)。
\(Rollout\ Worker\) 和 \(Forward\ Pass\ GPU\) 进行紧密的交互通信,每个 \(Rollout\ Worker\) 约 \(0.25s\) 将 \(observation\) 发到 \(Forward\ Pass\ GPU\) 并从 \(Forward\ Pass\ GPU\) 获取 \(action\)。
同时,每个 \(Rollout\ Worker\) 约每分钟发送 \(256\ samples\) (样本数据)到 \(Optimizer\ GPU\) 的 \(Exp.Buffer\) 中。
注意,\(Rollout\ Worker\) 从正在进行的游戏异步发送数据,而不是等待整个游戏完成后再发布数据进行优化。
Forward Pass GPU
\(GPU\) 集群,负责进行模型推理计算(前向传播),使用 \(observation\) 产生 \(action\),传回给 \(Rollout\ Worker\)。
\(Forward\ Pass\ GPU\) 不定时地从 \(Controller\) 拉取最新版本的模型参数,这些机器会轮询 \(Controller\),有新参数就拉取。
Optimizer GPU
\(GPU\) 集群,负责计算梯度、更新参数,其中包含经验池(\(Exp.Buffer\))。
\(Rollout\ Worker\) 将样本数据发送到 \(Exp.Buffer\) 中。
\(Optimizer\ GPU\) 含有 \(512\) 个 \(GPU\),每个 \(GPU\) 各自随机从经验池(\(Exp.Buffer\))中取 \(minibatch\) (\(1920\ samples\))计算梯度,每个优化步(约 \(2s\))使用 \(MPI\ allreduce\) 对所有 \(GPU\) 上的梯度计算平均值,每隔 \(32\) 个优化步(约 \(1min\))将优化后的新版本参数发给 \(Controller\)。
Controller
\(Redis\) 集群(分布式缓存数据库),负责保存各版本的模型参数以及系统状态数据便于控制集群的停止和继续。
3.2 时间划分
在 \(Openai\ Five\) 的训练过程中,\(Rollout\ Worker\) 不是收集整场游戏的数据再发送给 \(Optimizer\ GPU\),而是以较短的片段发送数据,该片段又可细分为 \(16\) 个样本(\(samples\)),每个样本又由 \(16\) 个决策步(\(time/ steps\))组成,每个策略步对应四个游戏帧(\(game\ frames\)),具体如下图所示:
3.3 自博弈
在自博弈训练中,我们不断地将智能体当前的最佳版本与自身或旧版本进行比较,并针对可能击败这些过去和现在的对手的新策略进行优化。
在训练时,\(Openai\ Five\) 在 \(80\)% 的游戏中是与最新版本参数的智能体进行对战,\(20\)% 的游戏中是和过去的版本对战。设定偶尔会与过去的参数版本进行对战,其目的是获得更稳健的策略并避免策略崩溃,即代理忘记如何与各种各样的对手比赛,因为如果只与最新的参数版本对战,它只需要学会一组很局限的策略来击败其最近的过去版本就可以了。
这里对于自博弈历史模型的管理,主要包含两个部分:
● 对手评分: 每个历史模型有一个质量评分 \(q_i\),在训练中,如果历史模型击败了最新的模型,则评分不变,如果历史模型输给了最新的模型,则按照下式更新历史模型的质量评分 \(q_i\)(其中 \(\eta\) 为学习率常数,固定为 \(0.01\)):
● 对手采样:根据质量评分 \(q_i\),按照 \(softmax\) 分布进行采样选取对手。
如下图所示为早期训练中历史模型的采样概率分布:
从上图可以看出,随着训练的进行(\(Version\) 的增加),模型能力在增长。分布的扩展很好地展示了智能体改进的速度:在训练最开始的时候,当智能体快速提升能力时,比较旧的对手就没什么训练价值,分数非常低(左上角的图:\(Version\) \(=\) \(1001\) \(Distribution\));在训练一段时间之后,当智能体能力提升较慢时,智能体会与各种各样的过去对手进行对战(右上角的图:\(Version\) \(=\) \(14001\) \(Distribution\))。
值得注意的是,这里在能力评估上引入的是一个额外的评分机制,而没有使用评估模型能力的 \(Trueskill\),可能是出于性能上的考虑。
3.4 探索机制
在强化学习中,“探索”是一个非常重要且被广泛研究的问题。在 \(Openai\ Five\) 中,鼓励智能体进行探索的方式可总结为三点:引入熵奖励(\(entropy\ bonus\))、引入团队精神参数(\(team\ spirit\))和游戏环境的随机化,接下来分别说明。
引入熵奖励
熵奖励以如下形式被添加到 \(PPO\) 的损失函数中(其中 \(c\) 是一个称为熵系数的超参数):
在训练过程的初始阶段,我们将熵系数(\(Entropy\ coeficient\))设置为初始值并在训练期间降低它,我们发现使用熵奖励可以防止过早收敛到次优策略。如下图所示为在训练早期,在不同的熵系数设定下,\(TrueSkill\)(表示智能体学会的游戏能力)和 \(speedup\)(表示智能体的学习速度)的变化情况。
从图中可以看出,较低的熵系数表现较差,因为模型更难探索;太高的熵系数表现更差,因为动作太过随机;\(Entropy\ coeficient = 0.01\) 时表现最佳。
引入团队精神
我们引入了超参数团队精神(\(Team\ Spirit\))来控制智能体是针对个人奖励还是团队共享奖励进行优化。如下图所示为在训练早期,在不同的团队精神设定下,\(TrueSkill\)(表示智能体学会的游戏能力)和 \(speedup\)(表示智能体的学习速度)的变化情况。
从图中可以看出,在训练的早期,团队精神越低表现越好,一开始团队精神为 \(0\) 是表现最好的,很快就被团队精神为 \(0.3\) 和 \(0.5\) 的对照组超越。可以预计在训练的后期,团队精神为 \(Team\ Spirit = 1.0\) 的效果最好,因为这样将会优化我们实际感兴趣的奖励信号(最终的团队优势)。
游戏环境随机化
在实际的训练环境中,对游戏的以下五个部分进行了随机化操作:
● 初始状态:在 \(rollout\) 游戏中,英雄开始时会获得带有随机扰动的起始等级、经验、金币、护甲、移动速度、生命再生、法力再生、魔法抗性、力量、智力和敏捷性等数值。
● 线路分配:在某个阶段,我们注意到智能体倾向于在一条路上抱团大乱斗,我们认为这代表了一种局部最优:具有较高的短期奖励,但由于其他路的资源丢失而导致长期奖励较低。因此引入了线路分配机制,将每个英雄随机分配各路,并因离开分配的线路而给予惩罚。然而,后续的消融实验表明这样的考虑可能没有必要。
● 肉山血量:肉山是大型中立生物。在训练初期,我们的智能体根本无法对付他,以至于到后来,他们就已经记住了永远不要接近这种生物的教训。为了让智能体学会在合适的时机去打肉山,我们将肉山的生命值随机化在零和满值之间,使其更容易(有时)被杀死。
● 英雄阵容:在每场训练对战中,我们都会从英雄库中随机抽取英雄组成阵容。
● 物品选购:物品选购是依靠脚本实现的:在推理模式下,我们的智能体总是严格按照脚本为每个特定英雄购买相同的物品;在训练模式下,对物品选购进行随机化,在脚本规定的物品集中添加、替换或删除一些物品。通过这种方式,期望我们的智能体在对抗灵活选购物品的人类时能有更好的表现。
4 超参数
4.1 超参数总结
下表给出了 \(Openai\ Five\) 的超参数。其中有一些超参数在训练过程中进行了改变,对于有改变的参数,\(x→y\) 表示平滑的单调过渡(通常会在一到三天的训练时间内线性变化),而 \(x↔y\) 表示由于正在进行的实验或分布式系统波动而导致参数不太受控的变化。另外,表中一共给出了三组参数(三列):
● \(OpenAI\ Five\) 为耗时 \(10\) 个月的时间经历了多次手术(\(Surgery\))的训练;
● \(Rerun\) 为完成了 \(OpenAI\ Five\) 耗时 \(10\) 个月的训练之后,根据经验确定部分参数后重新开始进行的一次耗时 \(4\) 个月的训练;
● \(Baseline\) 为各个参数的默认值,该默认值主要用在改变特定参数的消融实验中,如后续在专门研究不同的 \(Batch\ Size\) 对于训练结果的影响的实验中,除了 \(Batch\ Size\) 的其余参数均采用 \(Baseline\) 中的参数设置。
在运行 \(Rerun\) 时,根据吸取的经验教训简化了超参数的改变规则,最后只更改了四个关键超参数,下表为这几个超参数的更改时间表。每个超参数更改都是在 \(1\) ~ \(2\) 天内逐渐进行的,相当于数千次迭代(下表中的时间是开始更改的时间)。
预先计划的超参数更改时间表包括进一步的更改,以使该次 \(Rerun\) 实验符合 \(OpenAI\ Five\) 的最终超参数(\(GAE\) 视野为 \(840\) 秒,团队精神为 \(1.0\),学习率为 \(1e-6\)),但在参数修改到这些值之前,\(Rerun\) 就达到了 \(OpenAI\ Five\) 的技术水平,因此并没有继续进行,下图为 \(Rerun\) 的超参数改变过程中 \(TrueSkill\) 的增长曲线:
4.2 Batch Size
将使用小规模实验评估增大 \(Batch\ Size\) 的好处。由于训练系统的计算能力很强,为了充分利用这些计算能力,需要增大 \(Optimizer\ GPU\) 进行模型训练的 \(Batch\ Size\)。在过往关于图片识别的监督学习研究中,增大 \(Batch\ Size\) 可以有效的提高训练效率,可以达到次线性的加速效果。
在 \(OpenAI\ Five\) 的深度强化学习场景中,增大 \(Batch\ Size\) 意味着生产数据和消耗数据的速度都需要提高。这里探索在生产的数据加倍(\(Rollout\ Worker\) 加倍),消费的数据加倍(\(Optimizer\ GPU\) 加倍)时,是否能用一半的物理时间(\(wall\ time\))来达到同样的训练效果(达到某个指定的 \(Trueskill\) 分)。
为了了解 \(Batch\ Size\) 如何影响训练速度,我们定义了一个参数加速度(\(speedup\))表征智能体达到各种 \(TrueSkill\) 的速度:
由于在训练中,对 \(Optimizer\ GPU\) 机器池中的梯度进行平均,所以实际的 \(Batch\ Size\) 是 \(Optimizer\ GPU\) 的数量和 每个 \(Optimizer\ GPU\) 的 \(Batch\ Size\) 的乘积,始终在每个 \(Optimizer\ GPU\) 上使用显存可以支持的最大 \(Batch\ Size\)。因此,为了改变总 \(Batch\ Size\) ,只能增加 \(Optimizer\ GPU\) 的数量。同时,也与之匹配地增加了实验中其他机器池的大小(\(Rollout\ Worker\)、\(Forward\ Pass\ GPU\) 等),这样增大 \(Batch\ Size\) 才能真正针对更多数据进行优化,而不是重复使用相同的数据。
实验结果如下图所示:
图中显示了训练过程中的 \(TrueSkill\) 以及通过增加批量大小获得不同 \(TrueSkill\) 阈值的速率来衡量对训练的加速(\(speedup\))。右图中的黑色虚线表示理想的线性加速(使用 \(2\) 倍的数据可实现 \(2\) 倍的加速)。左图中 \(baseline\) 为 \(123k\ timestep\),可以看出当训练持续进行的时候(\(parameter\ version\) 代表优化器迭代的步数),更大 \(batch\ size\) 的模型 \(Trueskill\) 的上升更陡峭,说明大 \(batch\ size\) 的训练效果更好。从右图可以看出,当 \(batch\ size\) 增加到 \(983k\ timestep\)的时候,达到 \(Trueskill\ 175\)分的加速倍率为 \(2.5\) 倍,即 \(8\) 倍的数据量带来了 \(2.5\) 倍的训练效率提升,这个加速比是远达不到线性提升的,但是依然还是有一定的效果;而目标 \(Trueskill\) 下降到 \(125\) 分甚至更低的时候,其加速倍率甚至低于 \(1.5\) 倍。可以想象,当训练目标设定的更低的时候,大的 \(batch\ size\) 带来的提升会更低(更少的 \(optimization\ step\)),然而这里 \(Trueskill\) 最高只看到 \(175\) 分,在完整训练过程中也属于早期,比最后达到的 \(250\) 分低很多,\(Trueskill\) 分目标设定更高的时候,大 \(batch\ size\)带来的提升会更为明显,但具体能加速多少,能否达到线性加速并无定论。
4.3 Data Staleness
由于一场游戏的时间很长,\(Rollout\ Worker\) 实际运行速度为 \(15FPS\),一局游戏结束的物理时间可能会接近 \(2\) 小时。\(OpenAI\ Five\) 是基于 \(PPO\) 进行训练,所以如果要严格遵守 \(on\ policy\) 的训练方式,也就意味着每两小时才能更新一次梯度信息。
但是在实际操作中,为了提高训练效率,生产数据的 \(Rollout\ Worker\) 和消费数据的 \(Optimizer\ GPU\) 之间是异步机制,\(Rollout\ Worker\) 基于当前最新的模型参数进行游戏,产生的数据上报到 \(Optimizer\ GPU\) ,并存放到一个经验回放的缓存区(\(Exp.Buffer\))里,\(Optimizer\ GPU\) 以固定速率从缓存区里取样本进行训练。由于优化所使用的 \(Batch\ Size\) 非常大,就导致很可能出现 \(Rollout\ Worker\) 上执行的 \(policy\) 与正在优化的 \(policy\) 不一致的情况,即不符合 \(on\ policy\) 的假设,从而影响训练效果。
因此,在 \(Rollout\ Worker\) 处,每隔一小段时间(\(34\) 秒,产生 \(16\) 个样本,每个样本是 \(16\) 个决策步,每个决策步是 \(4\) 个游戏帧),就向 \(Optimizer\ GPU\) 发送一次数据,\(Optimizer\ GPU\) 每 \(1\) 分钟左右更新一次参数。对于一个训练时间长达两个月的复杂问题而言,这种分钟级的更新和同步看起来是个小问题,但实际上这里异步带来的很小的时间上的差异对训练效果有明显影响。为了量化分析这个因素,引入一个指标——数据陈旧度(\(Staleness\)),其定义为:\(M-N\),其中 \(M\) 为当前 \(Optimizer\ GPU\) 在优化的参数版本,而 \(N\) 则为 \(Rollout\ Worker\) 生成样本的参数版本,显然 \(M \ge N\)。同时在 \(Rollout\ Worker\) 上加入一个队列来缓存发往 \(Optimizer\ GPU\) 的数据,通过控制这个队列的长度就可以在不改变训练架构和参数的情况下模拟不同程度的 \(Staleness\) 对训练效率的影响。
实验结果如下图所示:
图中\(Queue\ length\) 越长代表 \(Staleness\) 越高,可以看出,相同迭代轮次下 \(Staleness\) 越高,获得的 \(TrueSkill\) 就越低,\(speedup\) 也越低,所以在训练中,希望 \(Staleness\) 越低越好。
4.4 Sample Reuse
训练采用了经验回放机制,这样处理的目的是解决异步机制带来的问题,保证缓冲区中有数据可供训练,如果 \(Rollout\ Worker\) 没有产生新数据,就反复使用旧数据。
由于 \(Optimizer\ GPU\) 从经验缓冲区(\(Exp.Buffer\))中采样,因此同一条数据可能被重复使用多次。如果数据重复使用过于频繁,可能会导致过拟合。为了了解数据的重复使用会对训练带来怎样的影响,引入了一个指标——样本复用率(\(Sample\ Reuse\)),其定义为:
\(Sample\ Reuse\) 是用于衡量经验回放缓冲区中数据消耗和数据产生的相对速率,当其为 \(1\) 时,说明数据使用完就会被更新,要是远大于 \(1\),说明同一个样本会被训练多次,可能会导致过拟合等问题。实验中,通过减少 \(Rollout\ Worker\) 的数目来模拟不同的 \(Sample\ Reuse\),也尝试增加了 \(Rollout\ Worker\) 的数目来模拟 \(Sample\ Reuse\) 小于 \(1\) 的情况。
实验结果如下图所示:
从图中可以看出,样本复用率(\(Sample\ Reuse\))和训练效率成反比,复用率越高,训练效果越差,值得注意的是,小于 \(1\) 的样本复用率对训练有正向帮助,虽然不明显,考虑到当样本复用率为 \(0.5\) 时带来的额外 \(Rollout\ Worker\) 计算开销,可能并不划算。但这里充分说明了当前强化学习应用的通病——样本利用效率低。由于从经验回放缓冲区中均匀采样,当样本复用率为 \(1\) 时,并不意味着每个样本都刚好被训练了一次,而是有的可能训练 \(2\) ~ \(3\) 次,有的完全没有被采样到,当生成的样本远大于消耗的样本时,用于训练的样本之间的关联性更差,对训练更有利。
4.5 Entropy coeficient
在前文介绍探索机制时,介绍了熵系数(\(Entropy\ coeficient\))的消融实验,这里重复总结一下。
实验结果如下图所示:
从图中可以看出,较低的熵系数表现较差,因为模型更难探索;太高的熵系数表现更差,因为动作太过随机;\(Entropy\ coeficient = 0.01\) 时表现最佳。
4.6 Team Spirit
在前文介绍探索机制时,也介绍了团队精神(\(Team\ Spirit\))的消融实验,这里重复总结一下。
实验结果如下图所示:
从图中可以看出,在训练的早期,团队精神越低表现越好,一开始团队精神为 \(0\) 是表现最好的,很快就被团队精神为 \(0.3\) 和 \(0.5\) 的对照组超越。可以预计在训练的后期,团队精神为 \(Team\ Spirit = 1.0\) 的效果最好,因为这样将会优化我们实际感兴趣的奖励信号(最终的团队优势)。
4.7 GAE Horizon
信用分配(\(credit\ assignment\))一直是强化学习研究和应用中的关键问题,在 \(Dota2\) 的研究中,这个问题更为突出,\(Dota2\) 的决策过程很长,因此在一局完整的游戏中如何做好长期的信用分配是一个难点。
为了研究信用分配问题的影响,定义超参数 \(GAE\ Horizon\)(\(H\)),用于表示 \(reward\) 衰减所影响的时间范围:
其中,\(T\) 是每个决策步(\(timestep\))对应的实际时间(\(0.133\) 秒,相当于 \(7.5\) 的帧率),\(\gamma\) 为衰减系数。
实验中基于 \(H = 180\),即 \(\gamma = 0.9993\) 进行模型训练到一定程度后,以此为基础模型,修改为几个不同的 \(GAE\ Horizon\) 参数继续训练,实验结果如下图所示:
从图中可以看出,减小衰减系数(\(\gamma\)),即增加时间范围(\(GAE\ Horizon\)),使得训练胜率上升的更快,直到实验最长时间范围(\(6\) ~ \(12\) 分钟)。这意味着训练系统能够在长时间范围内比较准确地进行信用分配,并且能够学习在未来 \(6\) ~ \(12\) 分钟内最大化奖励的策略。
5 游戏相关
5.1 游戏数据交互
在 \(Rollout Worker\) 中,\(Dota2\) 的客户端有一个 \(Lua\) 的接口,用于编写机器人脚本,该接口可以查询游戏可见状态、提交机器人要执行的操作。这里将该接口改造为从游戏内获取状态和输入动作的接口,同时,通过在游戏中集成一个 \(gRPC\ server\) 的形式,实现远程调用的功能,这样就可以以 \(docker\) 容器的形式将 \(Dota2\) 的客户端运行在大量的 \(CPU\) 集群上,方便部署和调试。
同时值得注意的是,\(gRPC\ server\) 这里进行 \(step\)(将训练环境封装成了类似 \(GYM\) 的环境,这里的 \(step\) 与 \(GYM\) 中类似:执行 \(action\) 并返回下一个环境状态的观察)的时候,会阻塞并等待 \(Lua\) 接口模块返回状态,而游戏内核部分在返回了当前的游戏状态之后,也会阻塞并等待 \(gRPC\ server\) 返回对应的动作,所以客户端方面运行是一个同步的机制,对于大量高并发的计算环境来说,降低了系统设计上的难度。
5.2 反应时间
\(Dota2\) 游戏本身的帧率是 \(30\),\(OpenAI\ Five\) 的处理是每 \(4\) 帧一个 \(timestep\),相当于将帧率降为 \(7.5\),论文中称之为跳帧机制(\(frameskip\))。
为了允许模型采取精确定时的动作,动作设计上存在 \(delay\) 机制,它指示模型希望在跳帧期间的哪一帧上执行该动作。因此,如果需要,模型可以在特定帧采取行动,尽管在实践中我们发现模型没有学会这样做,只是简单地在跳帧开始时采取行动。
另外,游戏和 \(AI\) 模型间采取了错开一个 \(timestep\) 的方式进行通信(游戏在第 \(T\) 个 \(timestep\) 发出观测状态,并继续运行到第 \(T+1\) 个 \(timestep\),\(AI\) 模型将第 \(T\) 个 \(timestep\) 对应的动作返回给游戏),同时由于动作设计上的 \(delay\) 机制,所以实际上可以认为 \(OpenAI\ Five\) 的反应时间有 \(5\) ~ \(8\) 帧的延迟,如下图所示,换算成时间大概在 \(167ms\) 到 \(267ms\),而参考人类的平均反应时间在 \(250ms\) 左右,所以基本认为在反应时间上是比较公平的。
5.3 英雄池
目前 \(OpenAI\ Five\) 的主要限制之一是无法使用游戏中的所有英雄,也就是英雄池比较小,这主要是为了缩短训练时间并适当减小复杂度。这里对英雄池的大小对训练效果的影响进行研究,在不同英雄池大小的情况下,进行一段时间的训练。由于在测试环境中仅选用五个英雄,因此英雄池越小,则训练的情况就与测试的情况越接近,因此可以预期较小的英雄池会获得更好的表现。
实验结果如下图所示:
从图中可以看出,使用更多的英雄进行训练只会导致训练速度有些降低。使用 \(80\) 个英雄进行训练的加速系数约为 \(0.8\),这意味着早期训练的运行速度比只使用 \(17\) 个英雄慢 \(20%\)。据此,可以猜测如果在 \(Rerun\) 中使用全部计算资源对更大的英雄池进行训练,获得近似水平所需的训练时间大约要多 \(20%\)(实际情况可能并不是如此,这里的实验仅对训练早期的情况进行了模拟)。另外,上图中关于英雄池大小为 \(5\) 的情况中 \(4000\) 次迭代之前能力增长的异常情况原因不明。
6 能力评估
6.1 TrueSkill
虽然与人类对战评估是最终的目标,但我们还需要在训练期间以自动化方式不断评估智能体,这就需要有一个更为量化且可持续的评估手段来评估训练进展。在 \(OpenAI\ Five\) 中,具体使用的是 \(Trueskill\) 评级系统,通过与具有已知能力水平的固定参考智能体池进行比较来进行能力评估。
在我们的 \(TrueSkill\) 评估系统中,评级 \(0\) 对应于随机动作的智能体,两个智能体之间 \(8.3\) \(TrueSkill\) 的差异大致对应于该智能体对另一个智能体拥有 \(80%\) 的胜率。
我们首先建立一个由许多具有已知技能水平的参考智能体组成的池,通过在各个参考智能体之间进行很多场比赛来评估参考智能体的 \(TrueSkill\)。我们编写的手工脚本智能体可以击败初学者,但不能击败业余玩家,其 \(TrueSkill\) 约为 \(105\),大概类似与普通人机吧。
另外,在训练过程中,我们尝试只与水平相近的参考智能体进行游戏,以获得最大程度的有用信息,避免与差距超过 \(10\) \(TrueSkill\) 的智能体进行对战(对应于胜率低于 \(15%\) 或高于 \(85%\))。
\(OpenAI\ Five\) 的 \(TrueSkill\) 随训练时间变化曲线如下图所示:
其中标记了 \(OpenAI\ Five\) 开始击败各种对手(从随机对手到世界冠军)的位置。我们推测,训练早期 \(TrueSkill\) 迅速增加到 \(200\) 是由于规模的指数性质:大约 \(8.3\) 的恒定 \(TrueSkill\) 差异对应于 \(80%\) 的胜率,并且学会如何持续击败比较弱的智能体是相对容易的。
在训练的早期,\(Trueskill\) 的增长非常迅猛,大概在总训练量的 \(10%\) 左右,\(OpenAI\ Five\) 能力已经可以和半职业队伍一较高低了(\(casters\)),后续 \(90%\) 的训练量只是推动了 \(Trueskill\) 分从 \(230\) 分增加到 \(250\),达到了可以击败 \(OG\) 的水平。
6.2 游戏风格
在游戏风格上,早期的 \(OpenAI\ Five\) 倾向于大规模团战,企图通过团战来取得前期优势,但是缺乏运营能力,导致对局结果两极化,要么前期快速胜利,要么前期崩盘,后期没有还手之力。随着训练的进展,\(OpenAI\ Five\) 的对战风格发生了明显的变化,主要的关注点从团战转移到了资源的积累上,学会了在优势英雄身上积累资源,不仅学会了通过大规模团战积累资源,也学会了在劣势情况下避免开团。最终版本的 \(OpenAI\ Five\) 与高水平人类选手的行为已经比较接近了,但是还是存在部分有特点的行为,如频繁的在地图上前后移动,更喜欢采取一些高风险的操作,如低血量的时候采取更有侵略性的行为,同时 \(OpenAI\ Five\) 也更愿意消耗手上的资源,如更愿意使用 \(CD\) 长的技能(比如 \(R\) 技能或者闪现等),而相比之下人类选手更愿意保留这些技能直到遇到更好的机会。
6.3 意图理解
对应论文附录 \(D\)。
7 手术机制
对应论文 \(3.3\),\(4.2\) 和附录 \(B\)。