本文将循序渐进地介绍业界主流的自动生成神经网络模型的 NAS 算法,以及目前最为落地的 ENAS 算法,带你一探 ENAS 究竟。
更多干货内容请关注微信公众号“AI 前线”,(ID:ai-front)
随着机器学习算法的突破,AutoML 逐渐被业界所熟知并开始落地应用。Google 近期频频发布 AutoML 相关的论文,并且在 Google Cloud 平台推出了 AutoML Vision 服务,此服务允许用户上传少量标记过的图片数据即可重新训练一个专属的图像分类模型。在我微博中大概介绍了这项“AutoML”技术,实际上可以通过 Fine-tune 等方式实现,因此抛出本文第一个观点,AutoML 的定义没有强制标准,只要能自动训练出好的模型就是有价值的 AutoML。
ENAS 全称 Efficient Neural Architecture Search,出自 Google 在 2018 年发布的论文《Efficient Neural Architecture Search via Parameter Sharing》(论文链接:https://arxiv.org/pdf/1802.03268.pdf ),通过共享模型参数的形式高效实现神经网络模型结构的探索,也就是说 可以在一天内自动训练得到一个比人类设计效果更好的模型。ENAS 使用了强化学习、动态构建计算图等技术,从宏观角度看是一种搜索算法,产出是一个精心挑选的 NN 模型,因此这是一种很符合期望的 AutoML,除此之外要实现完整的“自动 (Auto) 机器学习 (ML)”还需要有自动特征抽取、自动超参调优等技术支持,但暂时不在本文的讨论范围。
对于 ENAS 的介绍,大家可能已经看过媒体界吹捧的《谷歌大脑发布神经架构搜索新方法:提速 1000 倍》、《Jeff Dean 等人提出 ENAS:通过参数共享实现高效的神经架构搜索》等文章。这里抛出本文第二个观点,看媒体翻译的技术文章没有用,想学习技术可以看论文原文或者本文。在介绍 ENAS 算法实现细节之前,希望可以循序渐进地介绍一些前置的基础知识理解。
NAS 也就是 Neural Architecture Search,目标是从一堆神经网络组件中,搜索到一个好的神经网络模型。我们知道神经网络模型是一种可任意堆砌的模型结构,基础的组件包括 FC(全连接层)、Convolution(卷积层)、Polling(池化层)、Activation(激活函数)等,后一个组件可以以前一个组件作为输入,不同的组件连接方式和超参配置方式在不同应用场景有不同的效果,例如下面就是在图像分类场景非常有效的 Inception 模型结构。
图内结构比较复杂不理解也没关系,我们只需要知道这个神经网络结构是由图像领域专家花费大量精力设计出来的,并且经过了巨量的实验和测试才能(在不能解释深度学习原理的情况下)确定这个网络结构。
那么计算机是否可以自己去学习和生成这个复杂的网络结构呢?目前是不行的,包括各种 NAS 算法的变形还有 ENAS 算法暂时也无法生成这样的网络结构,这里抛出本文第三个观点,绝大部分机器学习都不是人工智能,计算机不会无缘无故获得既定目标以外的能力。因此,计算机并不是自己学会编程或者建模,我们还没有设计出自动建模的数据集和算法,所谓的“AI 设计神经网络模型”,其实只是在给定的搜索空间中查找效果最优的模型结构。
例如我们假设模型必须是一个三层的全连接神经网络(一个输入层、一个隐层、一个输出层),隐层可以有不同的激活函数和节点个数,假设激活函数必须是 relu 或 sigmoid 中的一种,而隐节点数必须是 10、20、30 中的一个,那么我们称这个网络结构的搜索空间就是{relu, sigmoid} * {10, 20 ,30}。在搜索空间中可以组合出 6 种可能的模型结构,在可枚举的搜索空间内我们可以分别实现这 6 种可能的模型结构,最终目标是产出效果最优的模型,那么我们可以分别训练这 6 个模型并以 AUC、正确率等指标来评价模型,然后返回或者叫生成一个最优的神经网络模型结构。
因此,NAS 算法是一种给定模型结构搜索空间的搜索算法,当然这个搜索空间不可能只有几个参数组合,在 ENAS 的示例搜索空间大概就有 1.6*10^29 种可选结构,而搜索算法也不可能通过枚举模型结构分别训练来解决,而需要一种更有效的启发式的搜索算法,这种算法就是后面会提到的贝叶斯优化、增强学习、进化算法等。
前面提到 NAS 是一种搜索算法,是从超大规模的搜索空间找到一个模型效果很好的模型结构,这类问题我们也可以看作一个优化问题,也就是从搜索空间中找到另一个模型效果更优的模型结构。而神经网络模型的结构和效果并不是线性关系的,也没有一个函数可以描述这种关系,如果存在这样的函数我们就可以通过求导、SGD 等方法用数学的方式找到最优解了,因此这可以看作一类黑盒优化(Black-box optimization)问题,或者是多臂老虎机(Multi-armed bandit)问题,对于黑盒优化问题的解法可以参考专栏另一篇文章《贝叶斯优化: 一种更好的超参数调优方式》(https://zhuanlan.zhihu.com/p/29779000) 。
回到 NAS 的场景,我们需要定义一个搜索空间来让算法选择更优的网络结构,而这个搜索空间无外乎就是网络层个数、网络层的类型、网络层的激活函数类型、网络层是否加 Dropout、网络层是否加 Batch normalization,还有就是诸如卷积层的 filter size、strip、padding 和池化层的 polling type、kernel size 等等,这些我们都可以认为是神经网络的超参数。
如果你使用过我开源的 tensorflow_tempalate_application 应该就理解,为什么是 template application?因为我们可以通过传入的参数 --model={dnn, lr, wide_and_deep, cnn}来选择更好的模型结构,也可以通过参数 --optimizer={sgd, adadelta, adagrad, adam, ftrl, rmsprop}来选择更好的优化算法等等,这里不再细说,感兴趣可以在 Github 上了解下:
https://github.com/tobegit3hub/tensorflow_template_application
实际上,NAS 可以是一个普通的超参调优问题,也可以是一个针对模型结构场景的调优问题。例如 Google 开源的 NASnet 模型和 ENAS 算法就是在通用的超参调优上更进一步,当然并不是说后者的方法更优更先进,而是有些问题是超参不好描述的,例如用超参来表达每一层的输入是前面的哪一层,这里不妨推荐一篇文章介绍基于 Policy gradient 的 NAS 经典实现:
https://lab.wallarm.com/the-first-step-by-step-guide-for-implementing-neural-architecture-search-with-reinforcement-99ade71b3d28
对于可以用超参轻易描述并且实现的模型结构,实现 NAS 并没有那么难,我们不需要自己去实现类似 Policy gradient 的启发式调优算法(后面会详细介绍到),可以使用一些已有的超参调优库,只要自己实现机器学习模型的训练过程和返回最终的指标结果即可。
因此我尝试在 Google Vizier 的开源实现 Advisor 上使用 NAS 的功能,发现不到一百行代码就可以实现完整的自动生成网络结构功能,而且不需要服务端做任何修改,我们用几十行 Keras 代码或者是 TensorFlow 等任意的框架都可以使用,只需要提前描述好超参的搜索空间即可,示例代码我也提交到 Github 上了。
Github 链接:
https://github.com/tobegit3hub/advisor/tree/master/examples/keras
前面提到 NAS 算法是一种搜索算法,除了需要定义好搜索空间外,还需要有一种启发式的可以收敛的调优算法实现。因为一般生成网络模型的搜索空间都很大,如果搜索空间不大就可以暴力枚举了,而且生成一个模型要评估好坏就需要训练,这个成本也是非常大了。
这时我们想到的调优算法就是贝叶斯优化(Bayesian optimization)、粒子群优化(Particle swarm optimization)、Policy gradient、DQN 等等,这些算法都可以用于 NAS 内部的模型的优化。是的,NAS 内部也有一个模型,一般在论文或者代码中称为 controller 模型,又它来控制想要生成的神经网络模型的结构,而生成的神经网络模型会根据客户的使用场景(例如图像分类)来返回模型效果对应的指标,这个一般称为 child 模型。
前面介绍了基于 Advisor 来实现的 NAS,其实就是使用了 Advisor 内置的调优模型作为 NAS 的 controller 模型,默认就是贝叶斯优化,而且只需要修改一个参数就可以通过统一的接口来使用后端已经支持的 Random search、Grid search 等调优算法。
前面推荐的博客使用的是增强学习中的 Policy gradient 算法,包括后面提到的 ENAS 论文也是使用这个算法,而在 Google 的其他论文中有介绍使用 Evolution algorithm 和 DQN 的,AlphaZero 在超参调优中使用的是贝叶斯优化,而 Deepmind 最新的调参论文介绍了类似 PSO 的群体优化算法。
大家需要理解这些算法从本质上没有区别,都是黑盒优化算法,因为假设不知道优化的目标和优化的参数是否有函数关系并且不能从中获取梯度等信息,只能从历史的模型训练结果中根据超参组合还有最后模型效果来选择超参的变化值。而 这些算法的区别在于运行效率和算法复杂度上,因此也会应用到不同的领域上。
我们经常看到下围棋、打游戏的人喜欢用 DQN、A3C 算法来优化模型,是因为下棋和打游戏都是可以通过软件模拟操作并且可以快速收到 reward 的,例如一分钟收集到几十万的 state 和 reward 完全足够训练一个高度复杂的 AlphaZero 模型(基于 ResNet)。而 AlphaZero 的超参调优却使用基于高斯过程的贝叶斯优化,是因为调整超参需要重新训练模型代价很大,大概几小时才能得到一个 state 和 reward,这不足以训练一个比 ResNet 弱很多的神经网络模型,但可以通过在数学上假设所有超参之间符合联合高斯分布然后通过高斯过程求 state、reward 关联关系的均值和方差,然后基于 exploration/exploitation 原则选择下一个更优的探索点,这也就是贝叶斯优化的实现原理了。这其实也解释了为什么 Google 以前基于 DQN、进化算法的 NAS 模型需要几百块 GPU 并发训练多个月,因为评估一个模型结构是好是坏真的很耗时,这也是为什么 ENAS 解决评估模型这个问题后这么 Efficient,后面会有详细的介绍。
下面有一个更直观的图介绍为什么贝叶斯优化在数学上会比瞎猜(Random search 算法)更好。首先红色的点表示已经探索过超参数,这里只有一个超参数 X 需要调整,Y 轴表示选择这个超参数 X 后模型的效果,可以认为 Y 就是模型正确率也就是越大越好,蓝线真实的超参 X 与正确率 Y 的对应关系,因为我们不可能遍历所有 X 因此蓝线我们是未知的,而我们的目标是已知几个红点去找到蓝线中的最高点。其中上图有一条虚线和浅蓝色区域,那就是贝叶斯优化中高斯过程返回的均值和方差,这个均值和方差是在 X 取值范围内都有的,分别代表了这个 X 点期望的正确率是多少和这个 X 点可能浮动的空间。
如果仔细观察可以发现,红点必然会在虚线上,因为这个点是已经探索过的,也就是用这个 X 去训练模型已经得到了正确率,因此这个点也必然在蓝线上。而浅蓝色区域代表了这一块可能浮动的空间,因此浅蓝色区域里虚线越大表示这块越值得 exploration,可能发现新的高点也不一定,而虚线越高表示对这块期望的 Y 值也越大,也是越值得 exploitation。为了权衡 exploration 和 exploitation 因此有了下面的 Utility function,这也是一种算法。我们可以简单认为它就是用均值加上λ倍的方差得到的曲线,然后取最高值作为下一个探索点,因为这个探索点代表了这个 X 位置期望的 Y 值还有想要探索更多未知的可能性,如果觉得比较难理解建议先参考《贝叶斯优化:一种更好的超参数调优方式》(https://zhuanlan.zhihu.com/p/29779000 )。
前面一直介绍的是 NAS,为了让大家了解 NAS 只是一种搜索算法。NAS 没有学会建模也不能替代算法科学家设计出 Inception 这样复杂的神经网络结构,但它可以用启发式的算法来进行大量计算,只要人类给出网络结构的搜索空间,它就可以比人更快更准地找到效果好的模型结构。
ENAS 也是一种 NAS 实现,因此也是需要人类先给出基本的网络结构搜索空间,这也是目前 ENAS 的一种限制(论文中并没有提哦)。ENAS 需要人类给出生成的网络模型的节点数,我们也可以理解为层数,也就是说人类让 ENAS 设计一个复杂的神经网络结构,但如果人类说只有 10 层那 ENAS 是不可能产出一个超过 10 层网络结构,更加不可能凭空产生一个 ResNet 或者 Inception。当然我们是可以设计这个节点数为 10000 或者 1000000,这样生成的网络结构也会复杂很多。那为什么要有这个限制呢?这个原因就在于 ENAS 中的 E(Efficient)。
大家大概了解过 ENAS 为什么比其他 NAS 高效,因为做了权值共享 (Parameter sharing),这里的权值共享是什么意思呢?是大家理解的模型权重共享了吗,但 ENAS 会产生和尝试各种不同的模型结构,模型都不一样的权重还能共享吗?实际上这里我把 Parameter sharing 翻译成权值共享就代表则这个 Parameter 就是模型权重的意思,而不同模型结构怎样共享参数,就是 ENAS 的关键。ENAS 定义了节点(Node)的概念,这个节点和神经网络中的层(Layer)类似,但因为 Layer 肯定是附着在前一个 Layer 的后面,而 Node 是可以任意替换前置输入的。
下面有一个较为直观的图,对于普通的神经网络,我们一般每一层都会接前一层的输入作为输出,当然我们也可以定义一些分支不一定是一条线的组合关系,而 ENAS 的每一个 Node 都会一个 pre-node index 属性,在这个图里 Node1 指向了 Node0,Node2 也指向了 Node0,Node3 指向了 Node1。
事实上,ENAS 要学习和挑选的就是 Node 之间的连线关系,通过不同的连线就会产生大量的神经网络模型结构,从中选择最优的连线相当于“设计”了新的神经网络模型。如果大家理解了可能觉得这种生成神经网路的结构有点 low,因为生成的网络结构样式比较相似,而且节点数必须是固定的,甚至很难在其中创造出 1x1 polling 这样的新型结构。是的,这些吐槽都是对的,目前 ENAS 可以做的就是帮你把连线改一下然后生成一个新的模型,但这个就是 ENAS 共享权重的基础,而且可以以极低的代码量帮你调整模型结构生成一个更好的模型,接下来就是本文最核心的 ENAS 的 E(Efficient)的实现原理介绍了。
我们知道,TensorFlow 表示了 Tensor 数据的流向,而流向的蓝图就是用户用 Python 代码定义的计算图(Graph),如果我们要实现上图中所有 Layer 连成一条直线的模型,我们就需要在代码中指定多个 Layer,然后以此把输入和输出连接起来,这样就可以训练一个模型的权重了。当我们把上图中所有 Layer 连成一条直线的模型改成右边交叉连线的模型,显然两者是不同的 Graph,而前一个导出模型权重的 checkpoint 是无法导入到后一个模型中的。但直观上看这几个节点位置本没有变,如果输入和输出的 Tensor 的 shape 不变,这些节点的权重个数是一样的,也就是说左边 Node0、Node1、Node2、Node3 的权重是可以完全复制到右边对应的节点的。
这也就是 ENAS 实现权重共享的原理,首先会定义数量固定的 Node,然后通过一组参数去控制每个节点连接的前置节点索引,这组参数就是我们最终要挑选出来的,因为有了它就可以表示一个固定神经网络结构,只要用前面提到的优化算法如贝叶斯优化、DQN 来调优选择最好的这组参数就可以了。
那评估模型也是先生成多组参数,然后用新的网络结构来训练模型得到 AUC 等指标吗?答案是否定的,如果是这样那就和普通的 NAS 算法没什么区别了。因为训练模型后评估就是非常不 Efficient 的操作,这里评估模型是指各组模型用相同的一组权重,各自在未被训练的验证集中做一次 Inference,最终选择 AUC 或者正确率最好的模型结构,其实也就是选择节点的连线方式或者是表示连线方式的一组参数。
稍微总结一下,因为 ENAS 生成的所有模型节点数是一样的,而且节点的输入和输出都是一样的,因此所有模型的所有节点的所有权重都是可以加载来使用的,因此我们 只需要训练一次模型得到权重后,让各个模型都去验证集做一个预估,只要效果好的说明发现了更好的模型了。实际上这个过程会进行很多次,而这组共享的权重也会在一段时间后更新,例如我找到一个更好的模型结构了,就可以用这个接口来训练更新权重,然后看有没有其他模型结构在使用这组权重后能在验证机有更好的表现。
回到前面 TensorFlow 实现 ENAS 的问题,我们知道 TensorFlow 要求开发者先定义 Graph,然后再加载权重来运行,但定义 Graph 的过程与模型训练过程是分开的,而 ENAS 要求我们在模型训练过程中不断调整 Graph 的结构来尝试更好的 Graph 模型结构。这在声明式(Declarative)编程接口的 TensorFlow 上其实不好实现,而命令式(Imperative)编程接口的 PyTorch 上反而更好实现。当然这里可以为 TensorFlow 正名,因为 ENAS 的作者就提供了基于 TensorFlow 的 ENAS 源码实现,开源地址 https://github.com/melodyguan/enas 。我们也深入看了下代码,作者用了大量 tf.case()、tf.while() 这样的接口,其实是 在 TensorFlow 的 Graph 中根据参数来生成最终训练的 child 模型的 Graph,因此没有用 Python 代码定义所有 child 模型的 Graph 的集合,也不需要每次都重新构建 Graph 来影响模型训练的性能。
除了 Node 之间的连线外,ENAS 论文里面可以训练的超参数还包括上图中的激活函数,也是通过 controller 模型输出的一组参数来决定每个 Node 使用的激活函数,而这些激活函数不需要重新训练,只需要直接加载共享权重然后做一次 Inference 来参与模型评估即可。
很多人可能觉得目前共享的权重比较简单,只能实现矩阵乘法和加法,也就是全连接层的作用,但在图像方面会用到更多的卷积网络,也会有 filter size、polling kernel size 等参数需要选择。实际上 ENAS 已经考虑这点了,这些 Node 拥有的权重以及使用权重的方法被称为 Cell,目前已经提供乘法和卷积这两种 Cell,并且在 PTB 和 Cifar10 数据集上做过验证,当然未来还可以加入 RNN 等 Cell,甚至多种 Cell 一起混用,当然这个在实现上例如前置节点的连线上需要有更复杂的判断和实现。
对于 ENAS 的 controller 模型的参数调优,论文中使用的是增强学习中的 Policy gradient,当然正如前面所提到的,使用贝叶斯优化、DQN、进化算法也是没问题的。不过目前 ENAS 的 controller 模型和共享权重的实现放在了一起,因此要拓展新的 controller 提优算法比较困难,未来我们希望把更多的调优算法集成到 Advisor 中(目前已经支持 Bayesian optimization、Random search、Grid search,正在计划支持 Policy gradient、DQN、Evolution algorithm、Particle swarm optimization 等),让 ENAS 和其他 NAS 算法也可以更简单地使用已经实现的这些调优算法。
总体而言,ENAS 给我们自动超参调优、自动生成神经网络结构提供了全新的思路。通过权值共享的方式让所有生成的模型都不需要重新训练了,只需要做一次 Inference 就可以获得大量的 state 和 reward,为后面的调优算法提供了大量真实的训练样本和优化数据。这是一种成本非常低的调优尝试,虽然不一定能找到更优的模型结构,但在这么大的搜索空间中可以快速验证新的模型结构和调优生成模型结构的算法,至少在已经验证过的 PTB 和 CIFAR-10 数据集上有了巨大的突破。
当然也不能忽略 ENAS 本身存在的缺陷(这也是未来优化 ENAS 考虑的方向)。首先是为了共享权重必须要求 Node 数量一致会限制生成模型的多样性;其次目前只考虑记录前一个 Node 的 index,后面可以考虑连接多个 Node 制造更大的搜索空间和更复杂的模型结构;第三是目前会使用某一个模型训练的权重来让所有模型复用进行评估,对其他模型不一定公平也可能导致找不到比当前训练的模型效果更好的了;最后是目前基于 Inference 的评估可以调优的参数必须能体现到 Inference 过程中,例如 Learning rate、dropout 这些超参就无法调优和选择了。
最后总结下,本文介绍了业界主流的自动生成神经网络模型的 NAS 算法以及目前最为落地的 ENAS 算法介绍。在整理本文的时候,发现 NAS 其实原理很简单,在一定空间内搜索这个大家都很好理解,但要解决这个问题在优化上使用了贝叶斯优化、增强学习等黑盒优化算法、在样本生成上使用了权值共享、多模型 Inference 的方式、在编码实现用了编写一个 Graph 来动态生成 Graph 的高级技巧,所以要 读好一篇 Paper 需要有一对懂得欣赏的眼睛和无死角深挖的决心。
本文致谢 Hieu Pham、Melody Y Guan、Barret Zoph、Quoc V Le、Jeff Dean。
陈迪豪,第四范式先知平台架构师,曾在小米科技和 UnitedStack 担任基础架构研发工程师。活跃于 OpenStack、Kubernetes、TensorFlow 等开源社区,实现了 Cloud Machine Learning 云深度学习平台,Github 账号 https://github.com/tobegit3hub。
更多干货内容请关注微信公众号“AI 前线”,(ID:ai-front)