复现一篇深度强化学习论文之后,我学到了什么AI学院

来源: 论智 / 作者:Matthew Rahtz / 2018-04-12 23:21
对强化学习的论文复现一直是个具有挑战性的活儿,超参数、随机种子等变量以及环境的不同都会造成难以复制的结果。于是就会出现这种情况:

对强化学习的论文复现一直是个具有挑战性的活儿,超参数、随机种子等变量以及环境的不同都会造成难以复制的结果。于是就会出现这种情况:

原论文(左)vs 我的论文(右)

可以说是很形象了……不过这种方法的确能让深度学习爱好者们受益匪浅。近日,瑞士机器学习专家Matthew Rahtz就发表了一篇博客,记录了他的一个复现项目,重点写了其中的收获和感悟。论智将原文搬运如下,希望能对大家有所帮助。

深度强化学习中有许多巧妙的地方。去年,最令人印象深刻的实验之一是OpenAI和DeepMind使用人类反馈而不是传统的奖励信号来训练智能体。Learning from Human Preferences一文对其进行了详细介绍,原始论文为Deep Reinforcement Learning from Human Preferences。

很多人认为对论文进行复现是提升机器学习技巧的好方法,所以我决定试一试。的确,这是一项非常有趣的项目,但回顾整个过程,我发现这与我之前的设想有所不同。下面是我在这一过程中的几个主要收获,如果你也正尝试进行论文复现,也许会有所帮助。


首先,总的来说,强化学习比预想的更复杂

其中最重要的一点是强化学习非常敏感(sensitive)。想要实现正确的结果,涉及到很多细节。如果没有达到正确结果,就很难检查出哪个步骤出现了问题。

案例一:进行了基本安装之后,训练却没有成功。我想到了好几种可能造成这种问题的原因,但是都没有结果。经过几个月的思考,终于搞清楚问题的根源在于第一阶段中奖励和像素数据的归一化问题。即使后来我们明白了,但之前却没有任何线索指向这里:像素数据进入的奖励预测网络的准确性很好,然而我花了很长时间才意识到要检查预测的奖励,这才注意到了奖励归一化的错误。找出问题的真正原因通常都是偶然发生的,通常一个小问题就会发现真正的解决方法。

案例二:在进行最终代码清理时,我发现自己出现了dropout类型的错误。奖励预测网络把一对视频剪辑作为输入,每个剪辑通过两个共享权重的网络进行相同处理。如果你添加了dropout并且不小心在每个网络中为其分配了相同的随机种子,那么每个网络都会断开,视频剪辑无法进行相同处理。但事实证明,尽管网络预测的准确性看起来完全相同,但将它修复完全打断了训练。

看看哪个图有中断?是的,没有。(呵)

所以我认为,这是常见的情况(比如《Deep Reinforcement Learning Doesn’t Work Yet》一文,具体可阅读论智此前的文章《深度强化学习的弱点和局限》)。从中我学到了,在开始一项强化学习项目之前,你应该准备好相应对一道棘手的数学题一样应对这个问题。这并不想我目前为止的编程经验,而是一旦你被某个问题困住,几天之后就能解开谜团。这更像你试图解决一个难题,根本没有一个清楚的方向,唯一能做的就是不断尝试,直到找到能解决问题的关键点。

唯一的方法是不断尝试,并且保持对混乱现象的敏感。

一个项目中有许多要点,其中唯一的线索来自观察一些毫无意义的小事情上。例如,在某些时候事实证明,将帧与帧之间的差异作为特征能让结果变得更好。利用新特征继续探索虽然很吸引人,但我仍然不明白为什么在这个简单的环境中,现在和之前能有如此大的差异。只有继续思考这个疑惑,并且认识到采用帧之间的差异可以清除背景,才知道解决问题需要用到归一化。

我不确定在这方面还有什么更好的方法,但是目前我最好的猜测是:

  • 学习辨认困惑会对人造成什么感觉。我们经常会有“好像有哪里不对”的感觉,有时你知道是代码不够漂亮,有时你会担心在错误的事情上浪费太多时间。但是有时你会看到一些你不想看到的东西:迷惑。要正确认识迷惑的具体情况非常重要,你可以……

  • 养成“顺着迷惑进行思考”的习惯。有些问题可以暂时先忽略,但是让你迷惑的事情却不能忽视。一定要及时检查那些反常现象

任何时候都要做好被一个问题拖住好几个星期的准备,并且要有信心坚持下去,注意观察容易被忽视的小细节。


说到过去编程经验中的一些区别,第二个我学到的重要经验就是在长时间迭代中需要的思维方式不同

调试过程主要包括四个步骤:

  • 思考问题原因并收集证据

  • 基于证据形成假设

  • 选择最可能的假设,实施修复方案然后看效果

  • 重复上述步骤直至问题消失

在我此前做过的编程项目中,我习惯做出快速反馈。如果某个地方失败了,就改变一下思路,看看有什么变化。事实上,在需要快速反应的情况下,收集证据是比形成假设更便宜的方法。如果你能做到快速反应,那么就能快速缩小假设的范围。

如果你的项目需要跑10个小时,而你还沿用上述策略,那么你会很容易浪费大量时间。上一个方法不行?好的,我认为是这个原因。让我们试试另一种方法,看看会怎样。第二天一早来了一看:还是不行?好吧,那再试试这个。一星期后,问题还是没解决。

如果同时运行多种方法,可能会有些帮助,但是首先,除非你有计算机群集的资源,否则可能会花费大量云计算成本。其次,由于上面提到的强化学习的困难,如果迭代得太快,你可能永远也不知道真正需要的是什么样的证据。

将思维从“多试验少思考”转向“少实验多思考”会对生产力有大幅提高。当你不断迭代想找出问题时,你需要尽可能多地思考是假设的形成过程,想一想所有的可能性。一旦你彻底将假设填充完整,并且知道了哪些证据可以帮你最好的区别各种可能性,就继续这个实验(这一步真的非常重要,尤其对正在做side project的人来说)。

推动这种转变的关键一点就是要坚持记录详细的工作日志。如果有些项目很短时间就能完成,可以不记录日志。但是如果有好几个小时以上、并且容易忘记实验过程的项目,必须要记录。日志的形式包括:

  • 我现在正在做的具体输出是什么?

  • 把你所想大胆地写出来,比如目前的假设是什么,接下来要做什么

  • 记录当前正在运行的状态,并记录每一步需要回答什么问题

  • 运行结果(TensorBoard表格或任何重要的观察),将不同运行类别区分开(例如智能体训练的不同环境)

刚开始我的日志记录的十分分散,但是越到最后,我就越意识到“工作日志就是我此时此刻脑海里想的事情”。这很费时,但是是值得的。


为了从实验中得到尽可能多的收获,在项目的开始我会做两件事,它们在结束时对结果有很大的帮助。

首先,最大限度地提高每次运行时收集的证据数量。其中有一些明显的指标,如训练或验证的准确性,但花时间在项目开始前的头脑风暴并研究那些其他指标可能对潜在问题非常重要是很值得的。

我会有这个建议的一部分原因可能来源于“后觉偏见(hindsight bias)”,我知道哪些指标应该早些开始记录。我们很难预测哪些指标是有用的。不过,可能有用的是:

  • 对于系统中的每个重要元素,要思考能从其中测量出什么。如果有一个数据集,就测量它的尺寸增长速度。如果有一个数列,就测量其中的被处理的元素的速度。

  • 对于每个复杂的程序,要测量中间有多少不同的部分。如果有一个训练循环,测试每批运行需要多长时间。如果推理过程很复杂,二两每个子推断需要多长时间。这些时间对将来的性能调试有很大帮助,并且有时可以解决难以发现的错误。

  • 同样,要考虑不同组件的内存使用情况。小规模的内存泄露可以说明很多事情。

另一个策略是看看其他人都测量了什么。在深度强化学习领域,John Schulman在他的Nuts and Blots of Deep RL 演讲中提出了一些有用的tips。对于梯度策略方法,我发现了能够表示训练进程的特殊策略熵是一个良好的指示器,它比per-episode奖励更灵敏。

上图是健康的策略熵和不健康的策略熵的对比。最左边是失败的模型,它收敛到定熵(在一个子集中随机选择)。中间的模型是另一个失败的案例,它收敛到了零熵(每次选择相同的动作)。右边是成功的案例,从Pong训练运行中的策略熵。

当你发现记录中存在可疑迹象时,一定要重视起来,不要把它想做是低效率的数据结构(有几个月我忽略了每秒帧数在悄悄衰减,由此造成了多线程错误)。

如果你能在一个地方看到所有的指标,故障消除就会变得容易。我喜欢在TensorBoard上完成。用TensorFlow记录任意标准可能会很困难,所以可以试试easy-tf-log,它提供了一个简单的tflog(key, value)的简单接口,无多余装置。

另一种能从输出结果中得到更多收获的方法,是尝试提前预测失败

有了“后见之明”,在回顾整个过程的时候可以轻易地发现失败。但真正让人抓狂的是,在你意识到发生了什么之前,失败就已经很明显了。比如当你前一天开始运行,第二天回来查看发现它失败了,还没检查到底是哪儿不对,你就想到了:“啊一定是我忘了设置frobulator。”

巧妙的是,有时你可以提前发现这种“后见之明”。它的确需要花费一些精力,但是你只要在开始下一次运行之前花费5分钟思考一下哪里可能出错了。具体可以这样做:

  1. 问问你自己:“如果结果失败我会不会惊讶?”

  2. 如果答案是:“不是特别惊讶”,那么就想象一下如果错了,原因是什么。

  3. 把所有可能想到的错误修改过来。

  4. 重复上述步骤,直到第一个问题的答案是“非常惊讶”。

在这一过程中总会出现你没有预料到的失败,有时你仍然不可避免地忽视明显的事情,但是这至少可以减少某种你认为真的很愚蠢的失败。


最后,这个项目最大的surprise是它需要多长时间来完成,以及它需要多少计算资源。

第一点说的是日期时间(calendar time)。我最初的估计是,作为一个side project,它可能得需要3个月的时间,但实际上却花了8个月。造成这种情况的主要原因是我低估了每个阶段所需要的时间。很难说这种情况带来了什么结果,但是对于side projects来说,实际所花费的时间可能是你预计时间的两倍

更有趣的发现是每个阶段实际所需的时间。我最初的项目计划所需时间大致如下:

耗费这么长的时间并不是由于写代码,而是在debugging。事实上,即使在一个所谓的简单环境中,它的工作所需时间也是最初的4倍。

另外就是所需要的计算量。很幸运,我有权限进入我们学校的群集,虽然只有CPU,但是足以应对这些任务了。对于需要用刀GPU的任务或者当群集太繁忙时,我试过两个云服务:谷歌Compute Engine的VMs和FloydHub。

如果你只是想shell访问GPU,Compute Engine就足够了,但是我尽量都在FloydHub上进行。FloydHub是一款针对机器学习的云计算服务。运行floyd run python awesomecode.py和FloydHub,创建一个container,上传你的代码并运行。FloydHub两个厉害的地方在于:

  • Containers预装了GPU驱动程序和通用库。(都2018年了,我还用VM浪费了好几个小时边升级TensorFlow,边调整CUDA版本)

  • 每次运行后都会自动归档。对于每次运行,使用的代码、开启运行的指令、任意命令行的输出以及任何输出数据都会自动保存,并可以通过web界面进行索引。

第二个特点真的是太重要了。对于任何这种长度的项目,保存任何详细记录以及重现之前的实验能力,是非常必要的。版本控制软件可以起到帮助,但是管理如此大量的输出非常痛苦,其次,项目人员需要极其勤奋。(比如你开启了某个项目,然后做了小小的改变之后又重新运行了一遍。当你想确认第一次运行的结果时,还能记得当初的代码吗?)有了FloydHub,你能因此省去很多精力。

我喜欢FloydHub的其他原因:

  • 一次运行结束后,container会自动关闭。不用再检查它们是否完成运行了。

  • 付款比VM更直接。

但是,我要吐槽FloydHub的一点就是你不能定制containers。如果你的代码含有大量依赖包,你就得在每次运行前重新安装一遍。这样一来迭代得就慢了。不过解决办法是创建一个含有安装依赖包的“数据集”,然后将这些数据集里的文件在每次运行之前复制一遍。虽然有点奇怪,但是比处理GPU驱动程序好多了。

FloydHub比Compute Engine贵一点,到目前为止,对于配有K80 GPU的机器,FloydHub的花费为每小时1.20美元,而类似的VM为0.85美元一小时。除非预算非常有限,我认为FloydHub贵点还是很值得的。

最终,我的项目一共花费了:

  • 在Compute Engine上,GPU运行150个小时,CPU7700个小时(wall time × cores)

  • 在FloydHub上,GPU运行了292个小时

  • 在我们学校的群集GPU上运行了1500个小时(wall time,4 to 16cores)

最后,我震惊了……这8个月的项目竟然花了850美元(200美元用于FloydHub,650美元用于Compute Engine)。

但是在项目最后,我发现了一个恐怖的事:强化学习会非常不稳定,以至于你必须用不同的种子将每次运行重复多次,直到达到稳定的性能。

例如,有一次我觉得应该没什么问题了,我开始做该环境下的端到端测试。但是我无法还原曾经最简单的环境了,即训练一个点移动到正方形的中心。我回到FloydHub重新运行当初的三个副本,结果显示虽然我认为没有问题了,但是只有三分之一的次数成功了。

下面的数字可能会让你对所需计算量有个大致概念:

  • 使用16个workers的A3C,Pong需要大约10个小时的训练时间

  • CPU需要160个小时

  • 运行3个随机种子就需要CPU运行480个小时(20天)

在成本方面:

  • 在八核的机器上FloydHub需要每小时0.50美元

  • 十个小时收费大约5美元

  • 同时运行3个随机种子,就需要15美元

再提醒一遍,Deep Reinforcement Learning Doesn’t Work Yet这篇文章中的那种不稳定现在看来已经是正常现象,可以被人接受。事实上,即使是“五个随机种子也可能不足以支撑重要的结果,因为仔细选择后你可以得到不重叠的置信区间。”

我的意思是,如果你想真正理解一个深度强化学习项目,你要保证知道自己在做什么,清楚你需要花多少时间和成本去准备。


总的来说,复现一篇强化学习的论文还是一件很有趣的side project。但是回顾过去,想想通过复现论文提升了什么技能,我也在思考过去几个月真正的收获是什么。

首先,我的确感到我的机器学习工程设计能力提高了很多。如今我更有信心解决强化学习中的错误了;工作过程也更加顺利。从这篇论文中,我发现了自己需要学习一些关于分布式TensorFlow和异步设计的知识。

另一方面,我却没觉得自己对机器学习的研究能力有所提高(这是我在复现论文时的主要目的)。相比过程的实施,研究中更难的部分似乎是提出既有趣,又实际且易解释的观点,这些观点能让你付出的时间得到极大的回报。能得出这样有趣的观点首先要有巨大的词汇量,其次要对观点有好的品味(即了解什么样的工作对该领域是有用的)。

所以,我从这个项目中获得的主要收获就是,每当你想提升实践技巧或研究能力时,反复思考是很重要的。并不是说实践和研究没有关联,而是如果你在某一方面非常弱,你最好针对这个方面做些项目训练一下。

如果你想同时提高这两方面,最好的方法是不断阅读论文,直到找到你最感兴趣的东西,并且还带有简洁的代码,你可以试着实现并扩展一下。


如果你真的想做一个深度强化学习项目,这里有几条具体的建议:

选择你想复现的论文

  • 看那些moving parts少的论文,避免那种好几个部分需要同时进行的论文。

强化学习

  • 如果你正在做一个大型系统,其中包括强化学习算法,不要自己尝试安装强化学习算法。虽然过程很有挑战性,但是强化学习目前太不稳定了,遇到问题的时候你可能搞不清楚哪里出了问题。

  • 做任何事之前,要用一个基准算法确定智能体在环境中训练的难易程度。

  • 不要忘了对观察结果进行归一化。

  • 一旦你认为某个元素起作用了,就立刻写端到端的测试。成功的训练可能比你想象的更脆弱。

  • 如果你在OpenAI的Gym环境中工作,需要注意在-v0的环境下,25%的实践中当下的动作会被忽视,之前的动作会被重复(以减少环境的决定性)。如果不想有多余的随机性,你可以用-v4环境。同样要注意,默认环境只会从模拟器中每隔4帧抽取一次,以匹配之前DeepMind的论文。如果你不想这样的话可以用NoFrameSkip环境。如果想得到模拟器实际的结果,可以用PongNoFrameskip-v4

一般机器学习

  • 由于端到端的测试需要很长时间才能完成,如果之后要进行重构会浪费大量时间。我们要在第一次运行时就检查错误并试运行,而不是之后再重新编写代码,保存重构。

  • 初始化一个模型可能只会花费大约20秒的时间,因为语法错误会浪费很多时间。如果你不喜欢使用或者不能使用IDE,那么可以花点时间为编辑器配置一个linter。无论哪种方式,每当你遇到一个愚蠢的错误,都可以花点时间让linter在未来解决它。

  • 在用共享权重安装网络时,不仅仅需要注意dropout,还有批归化。

  • 在训练时总会看到内存使用量有规律地上升,这可能是由于你批量验证的尺寸太大。

  • 如果在将Adam用作优化器时看到奇怪的事情,那可能是因为Adam的动量有问题。可以试着使用没有动量的优化器,例如RMSprop,或者通过将β1设置为零禁用Adam的动量。

TensorFlow

  • 如果你想调试你图中的某个内部节点,试试tf.Print,它会打印每次图形运行时它的输入值。

  • 如果保存检查点只为了推理过程,你可以忽略优化参数以节省空间。

  • session.run( )会很耗费计算资源。在必要时在一个批量中进行多个运算。

  • 如果在同一个机器上运行多个TensorFlow实例,得到了GPU内存不足的报错,这可能是因为其中一个实例试着占用所有GPU内存,而不是因为你的模型太大了。这是TensorFlow默认的行为。如果想让TensorFlow只保存需要的内存,可查看allow_growth选项。

  • 如果想从一次运行的多个模块中访问计算图,那么应该可以从多个线程中访问同样的计算图,但是目前一次只允许单个线程访问。

  • 在用Python的过程中,你无需担心溢出问题。但是在TensorFlow你仍然要注意:


  1. > a = np.array([255, 200]).astype(np.uint8)

  2. > sess.run(tf.reduce_sum(a))

  3. 199

  • 在运用allow_soft_placement的时候,要注意当GPU无法使用时,返回到CPU。如果你无意中编出了无法在GPU上运行的代码,它会默默转到CPU上。例如:


  1. with tf.device("/device:GPU:0"):

  2.  a = tf.placeholder(tf.uint8, shape=(4))

  3.  b = a[..., -1]

  4.  

  5. sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True))

  6. sess.run(tf.global_variables_initializer())

  7.  

  8. # Seems to work fine. But with allow_soft_placement=False

  9.  

  10. sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=False))

  11. sess.run(tf.global_variables_initializer())

  12.  

  13. # we get

  14.  

  15. # Cannot assign a device for operation 'strided_slice_5':

  16. # Could not satisfy explicit device specification '/device:GPU:0'

  17. # because no supported kernel for GPU devices is available.

  • 我不知道有多少类似的操作无法再GPU上运行,但是以防万一,可以按以下命令手动返回CPU:


  1. gpu_name = tf.test.gpu_device_name()

  2. device = gpu_name if gpu_name else "/cpu:0"

  3. with tf.device(device):

  4.    # graph code

 

心理指导tips

  • 不要对TensorBoard上瘾,真的。下面就是一个上瘾的实例:大多数时间,你都在检查项目运行地如何,一般都没什么特殊情况。但是有一次当你再次检查时,突然发现了一个了不起的结果。如果你觉得必须每隔几分钟就要检查TensorBoard了,那最好给自己设定合理的检查时间。


如果想了解更多强化学习的知识,下面是一些推荐资料:

  • 《Deep Reinforcement Learning: Pong from Pixels》 by Andrej Karpathy。它很好地介绍了强化学习的构建动机和目的

  • 想了解更多关于强化学习的概念,可观看David Silver的视频。其中并未涉及很多深度强化学习的知识,但介绍了许多相关词汇。

  • 《Nuts and Bolts of Deep RL》by John Schulman。其中有一些很实用的tips。

如果想从宏观角度探索强化学习的前景,可以参考以下资料:

  • 《Deep Reinforcement Learning Doesn’t Work Yet》by Alex Irpan。对目前强化学习的现状做了总结。论智也曾对这篇文章作了报道,具体可见:《深度强化学习的弱点和局限》

  • 《Recent Advances and Frontiers in Deep RL》by Vlad Mnih。为上面Alex的文章增加了几个例子。

  • 《Deep Robotic Learning》by Sergey Levine。关于提高机器人动作生成和采样效率的演讲。

  • 《Deep Learning for Robotics》by Pieter Abbeel。在NIPS 2017上的演讲,展示了深度学习最近的成果。
     

阅读延展

1
3