分享

翻译 Tensorflow之旅

 万皇之皇 2017-12-31

Peter Goldsborough写的文章,我个人觉得很不错,就全文翻译过来,第一次发表在简书。原文在此,https:///pdf/1610.01178v1.pdf。

摘要:深度学习是人工智能使用深度神经网络架构的一个分支,它已经在计算机视觉,语音识别,自然语言处理等领域取得显著成果。2015年11月,谷歌开源其内部使用多年的用来定义,训练和发布机器学习模型的软件库,Tensorflow。本文将会把Tensorflow放到现代深度学习理论和软件实现的框架下评估。我们将讨论Tensorflow的基本计算范式和分布式计算模型,它的编程接口还有可视化组件。然后我们再把它和其它深度学习库,比如Theano,Torch或Caffe做定性和定量对比分析,最后对Tensorflow的已知的学术界和工业界使用实例作评估。

一,简介

现代人工智能系统和机器学习算法在各个领域都取得了革命性的进步。我们注意到在计算机视觉,语音识别和自然语言处理等技术领域的长足进步。更进一步,这些技术进步已经惠及我们个人生活的方方面面。个人定制数字化助理,电商平台的推荐系统,金融诈骗检测,定制化网页搜索和社交网络订阅以及新颖的地理位置经济学发现都得益于当前机器学习算法。

近几年,一种称之为深度学习的机器学习的分支已经被证明特别有效。深度学习属于表现学习算法类,它使用复杂神经网络结构,其中包含巨大数量的隐藏层。每个隐藏层包含对输入数据进行简单但却是非线性转换。给定足够这样的转换模块可以构成非常复杂的功能模型去实现分类,回归,翻译和很多其他的事情【1】。

值得注意的是,深度学习的崛起也就是近几年的事情,这都得益于越来越多可用的大数据集可以作为训练样本;得益于有效使用GPU和大量并行硬件在大数据集上训练深度学习模型;也得益于一些新方法的发现,比如修正线性单元(ReLU)激活函数或者退化(Dropout)作为一种正规化技术【1】【2】【3】【4】。

在深度学习算法和单独算法模块比如表示转换,激活函数,或者正规化方法在开始还是数学公式的时候,接下来它们必须要转化为某种计算机程序实现才能用于应用。因此,市场上存在很多这样的开源和商业软件。在这些众多的软件中,Theano【5】,Torch【6】,scikit-learn【7】和许多其它的软件会在本文第二部分有更详细的评估。而Tensorflow是在2015年11月由谷歌发布的全新机器学习软件库【8】。根据开始公布的情况来看,Tensorflow旨在担当在“大规模”异构分布式系统上的“快速机器学习算法实现”【8】。

本文接下来将完整评估Tensorflow,并且将其放在当前机器学习的大背景下来讨论。本文详细架构如下,第二部分将介绍机器学习软件发展历史,不局限于Tensorflow相似项目。接下来,第三部分将深入讨论Tenorflow的计算范式。在第四部分,我们会解释各类编程语言的编程接口。第五部分会介绍Tensorflow的调错可视化工具。第六部分是Tensorflow和其它类似软件库的定性和定量分析。在最后第八部分总结,第七部分对于Tensorflow的学术界和工业界使用做了研究。

二,机器学习软件库历史

本章旨在提供一个机器学习软件库历史和关键里程碑的描述。开始,我们回顾20年前一系列广泛用于机器学习和数据分析的软件库。然后,我们聚焦到最近适合于深度学习的编程框架研究。图一展示的是这一研究在时间轴上的展开。本章不会比较比较Tensorflow和这些软件库,因为这一部分将会在第六部分具体讨论。

A. 通用机器学习

在接下来的文字,我们将会列举和回顾一组通用机器学习软件库。所谓“通用”,意味着这些软件库用于机器学习和数据分析,并不局限于深度学习。所以,它们可能用于统计分析,聚类,降维分析,结构化预测,异常检测,浅神经网络(相对于深度神经网络而言)和其它。

让我们从Tensorflow发布21年前的一个库开始说起,MLC++【9】。MLC++是用C++开发的算法库,包含了一系列数据挖掘,统计分析,模式识别技术的比较框架。它诞生于斯坦福大学,现在被Silicon Graphics Inc公司(SGI)维护。据我们所知,这是迄今为止岁数最大的机器学习库。

排在MLC++后面的,当属Bradski等人在2000年发布的OpenCV【10】。它主要解决计算机视觉和图像识别问题,包括一系列人脸识别,对象识别,3D模型提取等。它遵循BSD开源,拥有C++,Python和MATLAB等多种编程接口。

另一种机器学习库我们希望提及的是scikit-learn【7】。2008年由David Cournapeu作为谷歌编码之夏项目的一部分开发出来。它是使用Python编写的开源库,基于NumPy,SciPy和matplotlib框架。它能够用于广泛的监督学习和非监督学习问题。

Accord.NET和前述库不一样,它是2008年使用C#开发的,除了一些机器学习算法,它还包括了用于语音识别和图像识别的信号处理模块。【11】

Massive Online Analysis(MOA)是一个可以支持在线和离线大数据分析的开源库。MOA包括了一系列用于分类,回归,推荐系统和其它训练能力。它于2010年发布,使用JAVA编程语言,由新西兰的Waikato大学维护【12】。

Mahout是Apache软件基金会支持的一个Java开源库,基于Hadoop平台,支持可伸缩的机器学习应用。它可以凭借Hadoop分布式文件系统(HDFS)对于大数据集的很好支持进行分析。Mahout提供了分类,回归和过滤算法。

Pattern在我们认可的Python机器学习库列表之中,得益于它提供丰富的网页挖掘能力。它包含了不仅仅是机器学习算法(比如,聚类,分类或者最近邻近搜索),自然语言处理(n-gram搜索用于情感分析),还提供了网络爬虫,比如抓去Tweets或者Wiki百科的数据来帮助快速分析。它由Antwerp大学于2012年开发,开源并维护。

最后一个需要介绍的是Spark MLib,也是一款开源机器学习库,发布于2015年,看名字很容易知道,这是基于Spark开发而成分布式分析系统。它支持处理大规模分布式数据集,并且在集群硬件环境下进行机器训练。它包含了分类,回归,聚类和其它机器学习的算法【14】。

B. 深度学习

上述软件库能够用于非常广泛的机器学习,统计分析应用。接下来介绍的库在训练深度模型方面特别有效。

首先介绍的是发布于2002年的,最古老的Torch【6】。Torch包含了一个纯C++实现和编程接口。现在,他的核心是C/CUDA实现,对外暴露的语言是Lua脚本语言。Torch采用一个LuaJIT(实时)编译器把Lua的函数过程连接到C语言的具体实现。它包括了内部别名,数值优化过程,神经网络模型,同时能够产生通用的N维数组(张量)对象。

Theano发布于2008年【5】,是另外值得一提的库。我们注意到Theano在机器学习社区具有极高的人气,但是它实际上并不是机器学习库。它只是符号化数理编程框架,比如生成计算图,然后编译和优化在CPU或者GPU上运行,所以Theano更多时候只是“数理编译器”。

Caffe是由Berkley视觉与学习中心(BVLC)维护的开源深度学习库。它于2014年发布,并遵循BSD开源【15】。Caffe使用C++实现,神经网络层是它的计算单元(这和Theano那些需要用户自己定义详细的数学公式去构成神经网络层不同)。包含很多层的深度学习模型采用谷歌协议缓冲格式保存(GPBF)。模型能够使用协议缓冲“语言”定义,然后通过和Python或者MATLAB已有的绑定,自动生成神经网络。Caffe特别适合于训练卷积神经网络(CNNs,或者ConvNets)用于图像识别领域。

上面介绍的机器学习库都是使用Python,MTATLAB或者Lua定义深度学习模型,而Deeplearning4J(DL4J)则是使用Java去创建深度神经网络。DL4J能够创建有限波资曼机,卷积神经网络和递归神经网络。DL4J还能够使用Hadoop或者Spark这样的分布式计算平台进行工作。Adam Gibson2014年编写的DL4J遵循Apache 2.0开源协议。

最后我们要介绍的是NVIDIA的深度学习SDK。这个SDK能够充分利用GPU来加速计算。SDK首先提供了基于GPU的高性能深度算法(cuDNN),用于加速卷积,激活函数和张量转换。其次,它提供了一个线性代数库,cuBLAS。最后,cuSPARSE包含了基于GPU的高效空间矩阵运算。它可以单独使用,也可以和其它库联合使用,比如Torch。

翻译 Tensorflow之旅

三, Tensorflow编程模型

在这一章,我们将深度讨论Tensorflow的计算范式。开始,我们会研究Tensorflow的基本架构和计算原理,解释机器学习算法如何在Tensorflow里面使用数据流图语言表征。接下来,我们研究Tensorflow的运行模型,也就是Tensorflow如何把Tensorflow图转化为运行状态。然后,我们调查Tensorflow内在的,针对软硬件的不同算法优化。最后,我们列举一系列算法扩展,以支撑用户在Tensorflow对于计算模型,逻辑模型训练。

A. 计算图架构

在Tensorflow里面,机器学习算法被表证为计算图。计算图或者数据流图是一种有向图。有向图的顶点或者节点代表运算过程,有向图的边代表运算过程之间的数据流。如下图所示,先看左边,输出变量z是输入x和y的二值运算的结果,那么画两条分别从x和y向z的有向边,并且在z上标明运算加号。一个更加完整和复杂的数据流图在右边。下面,对于数据流图里面的元素(operation - 算子,tensor - 张量,variable - 变量和session - 过程)进行更为详尽的讨论。

翻译 Tensorflow之旅

1)Operations(算子):使用图来表示一个算法的主要好处不仅仅是直观展示计算模型之间的依赖和关联,而且能够更普适的定义运算节点。在Tensorflow里,node(节点)代表着算子,也就是一种运算,更精确来说,代表了输入的数据在有向图上,如何流经这个节点【8】。一个算子可以是0到多个输入,也能产生0到多个输出。因此,一个算子代表一个数学等式,一个变量,一个常量,一个有向控制流,一个文件I/O操作或者甚至是一个网络通讯连接端口。在算子可以表达为一个常量或者变量不是像表达成一个函数那样直观,但是一个常量可以被当成一个没有输入,而且输出恒定的运算。相似的情况也适用于表征一个变量,也就是没有输入,输出当前状态或者当前变量的值。

任何算子必须要严格的定义和实现。在论文【8】的研究,任何一种实现被当作一个算子的核函数。一种特定的核函数的具体编程都是针对某种硬件,比如CPU或者GPU的运算。

2)Tensors(张量):在Tensorflow里面,代表数据流从一个算子流向另一个算子的边称之为张量。一个张量是一个具有固定类型同类数据的多维集合。张量维度也称为rank(阶)。张量的形状(shape)描述张量大小的元祖(tuple)。比如,对于每一个维度的元素个数。从数学的观点来看,张量是一个二维矩阵加一个表达张量阶的一维向量或者标量的生成物。

从计算图的观点来看,张量是表征通向输出的算子的符号把手(symbolic handle)。在内存里,张量本身并不包含和存储数据,但是它提供了张量代表的数据访问的接口。在Tensorflow里面创建一个算子的时候,比如x+y,返回一个张量对象。然后,这个张量可能作为其他计算的输入,从而张量是连接源算子和目的算子的边。基于这样的认知,数据在Tensorflow的图中川流不息。

除了通常意义的张量,Tensorflow还支持一种称之为稀疏张量(SparseTensor)的数据结构。这是一种空间有效字典类的数据表证,也就是大量零值的稀疏张量。

3)Variables(变量):在通常情况,比如做随机梯度下降的单次运算的时候,机器学习模型的图会从开始到结束反复运算多次。在两次调用之间,图中主要的张量并不会被保存。但是整体上对于图的求值是需要保存状态的,比如神经网络的权重和参数。因此,变量正是为了满足这一需求而创建的算子,能够被添加到计算图中。

变量可以看成是在内存中持久不变的张量副本。因此,变量的定义需要形状和固定数据类型两个特征。Tensorflow提供了一系列赋值函数完成图运算。

在Tensorflow的图中创建一个变量节点的时候,需要定义相应的张量,这样在图运行时,变量可以随之初始化。变量的形状和数据类型也来自于这个初始器。有趣的是,变量自己并不存储这个初始的张量,相反构造一个变量会增加三种不同的节点:

1)变量节点,保存持久状态。

2)保存初始值的算子,通常是一个常量。

3)初始器算子,在图求值的时候把初始值赋值给变量张量。

一个例子如下图所示:三个节点代表变量定义。第一个变量v是一个变量的张量值在内存中的持久副本。第二个变量i是给变量提供初始值(可以是任意张量)的节点。最后一个赋值节点把初始值赋给变量,在赋值节点产生一个新的张量具有初始值的变量v‘。这样,v'可能作为另外算子的一个输入。

翻译 Tensorflow之旅

4)Session(会话):在Tensorflow里面,算子的运算和张量的估值会在一定的上下文中进行,我们称之为Session(会话)。会话的责任之一就是将分配和管理资源的工作封装起来,比如缓存变量。更进一步来看,Tensorflow库里面的Session接口提供了一个run函数,作为整个图计算的执行入口。这个方法将输入节点带入整个图计算的过程,并且根据图定义返回相应的结果。另外,一个可选的映射,即从任意节点到相应替代值的映射,被称为feed nodes(反馈节点),可能也被run调用【8】。

在调用run的时候,Tensorflow将会出输出节点开始反向分析计算图的节点依赖,计算所有节点的传递闭包。这些节点可能被分配到一个或者多个物理计算单元(CPU,GPU等),这些计算单元可以在一台或者多台机器上。分配的规则由Tensorflow的placement algorithm(分配算法)定义。这个算法会在本文后面部分谈到。此外,因为存在节点评估顺序的特定显式可能性,姑且称为控制依赖关系,执行算法将确保这些依赖关系不变。

B. 执行模型

如刚刚讨论的那样,对于执行各种计算图元素的组成,TensorFlow划分其任务在四个不同的组中实现:客户端,主控端,一组工作进程和一些设备。 当客户端通过会话的run调用请求评估一个TensorFlow图,这个请求被发送到主控端,主控端将任务委托给一个或多个工作进程处理和协调它们的执行。 每个工作进程随后负责一个或多个设备真实的运算和处理操作。

在这个模型中,有两个扩展度。 第一扩展度是关于执行图运算机器的数量。 事实上,第二扩展度指的是在每台机器上,可能会有更多设备,例如,五个独立的GPU和/或三个CPU。 为此,存在两个“版本”的TensorFlow,一个用于在单个机器上本地执行(但可能有许多设备),一个支持分布式在许多机器和许多设备上实现。

翻译 Tensorflow之旅

上图展示了执行模型和响应扩展度。而且Tensorflow在开始发布的时候,确实只公布了单机版本,而多机版本是在2016年的4月13日才姗姗来迟【16】。

1)Devices(设备):设备是在TensorFlow执行模型中最小,最基本的实体。 上图中的所有节点,也就是每个算子的内核,最终都必须是映射到要执行的可用设备。 在实际执行过程中,设备通常是CPU或GPU。 然而,TensorFlow能够支持更多种类的物理执行单元。 例如,在2016年5月,谷歌宣布其定制的ASIC(专用集成电路)张量处理单元(TPU),是专门用于快速张量计算[17]。 因此,Tensorflow是可以容易地集成新出现的设备类新型硬件。

为了整体评估在某个设备上的节点,主控端生成相应的工作进程。 作为工作进程可以在单个机器上管理一个或多个设备,设备是不仅通过名称标识,还标识其所在工作进程组的索引。 例如,特定组中的第一CPU可以由字符串“/ cpu:0”标识。

2)placement algorithm(分配算法):为了决定哪一个节点分配给那一个设备,Tensorflow使用了一种特定分配算法。该算法模拟计算图的运算和遍历从输入张量到输出张量的全部节点。在遍历节点过程中,要决定哪一个可用设备D={d1,d2,...dn}运行给定节点v,算法使用一个成本模型Cv(d)。这个成本模型考虑四种信息来决定最优设备d = arg mind∈D Cν(d):

1)在给定设备上是否存在该节点的实现(核函数)。比如,如果任何一个GPU上都不存在某种特定算子,那么选择任意GPU的成本都是无限的。

2)估计一个节点的输入和输出丈量的大小(按字节计算)。

3)给定设备对于核函数的期望计算时间。

4)对于输入丈量到相应算子跨设备或者跨机器的传输成本进行启发式估算,万一该节点已经赋值的输入张量所在设备和当前设备并不一样。

3)cross-device execution(跨设备运行):只要是用户拥有多个设备,Tensorflow通常都会把节点分配到这些设备上去。这个过程是通过把节点分类,然后一类分配到一个设备。这样分配的话,必须处理跨设备分配的节点依赖问题。让我们考虑A和B这样两个设备,其中节点v在设备A上。如果v的输出张量作为另外两个算子α, β的输入,同时α, β在设备B上。那么就存在从A到B的跨设备的边ν → α 和 ν → β。如下图所示:

翻译 Tensorflow之旅

在实际运行中,需要使用一些传输手段完成v的输出张量从A设备,比如GPU,到设备B,比如CPU的过程。如下图所示,send和recv两类节点被生成完成这个传输过程。

翻译 Tensorflow之旅

最后,Tensorflow使用“规范化”来优化(send,recv)对。在上图所示的例子中,我们看到两个recv节点,分别连接α, β。然而,一种等价的,但是更为有效的办法就是在设备B上只运行一个recv,如下图所示。

翻译 Tensorflow之旅

C. 执行优化

为了保证Tensorflow运行模型的最优化,一系列优化算法被内置其中。本章节将重点介绍三种优化方法:常用子图消减法,执行时序安排法和损失压缩法。

1)Common Subgraph Elimination(常用子图消减法):常用子表证消减法是众多现代编译器采用的优化算法,凭借合并同类计算项的办法,编译器可以将相同计算的多次实例合并成一个实例。结果会保存在一个临时变量,这样可以重复使用该结果。在Tensorflow图的运算中,也会出现相似的情形,一个算子对于相同输入会被重复调用多次。如果就这样直接运算,那么效率是非常低下的,同时大量的内存占用也是不合理的。因此,Tensorflow也应用这种同类消减法,或者更恰当的描述是基于前次执行的子图消减法。对于这种算法,遍历计算图,凡是遇到两个或者更多对于同样输入张量的同种类型的算子可以通过规范化合并成一个子图。此算子的输出张量然后会被转移到所有相应依赖的节点去。下图给出了消减的一个示例。

翻译 Tensorflow之旅

2)Scheduling(时序安排法):一个简单而强大的优化是尽可能晚地调度节点执行。 确保操作结果仅在最小所需时间段内保持在存储器中,由此减少了存储器峰值消耗,并且因此可以大大提高系统的整体性能。文献 [8]的作者指出,这对于诸如GPU之类的设备尤为重要,因为它们的存储器资源很少。 此外,仔细安排的调度还涉及激活send和recv节点,这里面不仅存储器而且网络资源都被作为竞争资源。

3)Lossy Compression(损失压缩法):诸如分类,回归或者其它机器学习算法的主要目的是建立稳健的模型。我们这里说“稳健”的意思是指理想的优化过的模型算法不会因为偶然噪声信号改变响应输出。因此,算法对于计算数值的精度要求就会下降,比如使用16比特运算就可以达到32比特计算的效果,但是很明显这样不仅仅是节约了小数点后位数,也极大降低了计算开销。基于这个原则,在Tensorflow里面,把节点转化为计算图的内部加法采用了类似的方法。当数据通讯跨设备或者机器的时候,在发送端将32位浮点数字转化为截断的16位表示,在接收端被截断的数字被简单的通过补零法恢复成32位,而不是使用取整运算【8】。

D. 和基础编程模型相关部分

讨论了Tensorflow的基础计算范式和执行模型之后,我们将继续讨论另外三种和创建机器学习算法高度相关的高级主题。第一,我们讨论Tensorflow如何处理被许多机器学习算法广泛使用的梯度反馈(gradient back-propagation)的。然后,我们研究Tensorflow是如何控制数据流的。最后,我们简单谈一下检查点技术(checkpoint),因为在使用大型模型的时候非常有用的一种技巧和技术。

1)Back-Propagation Nodes(反向传播节点):在相当多的深度学习和其它机器学习算法里面,计算计算图中特定节点相对其他一个或者多个节点的梯度是非常有必要的。比如,在神经网络中,我们可能通过将给定样本进行一系列非线性转换来计算模型的成本函数c。如果神经网络包含两个隐藏层,其表证函数f(x;w) =fx(w)和g(x;w) =gx(w),w是内部权重。我们可以把其对应该样本的成本函数表示成c = (fx◦gx)(w) =fx(gx(w))。通常我们会做微分dc/dw来求相应的权重w,然后用得到的权重去更新原有权重。这种计算在反向传播算法是通过反向遍历图去计算[fx(gx(w))]′=fx′(gx(w))·gx′(w)完成的。

在文献【18】中,描述了两种计算反向传播梯度的算法。第一种称为symbol-to-number differentiation(图数差异法)。它是接受一组输入值,然后计算这组输入的梯度数值。它通过明确的前向遍历图计算成本函数,然后再反向遍历计算剃度。第二种称为symbol-to-symbol derivatives(符号到符号导数法),这是和Tensorflow更相关的算法,在文献【8】中被称为automatic gradient computation(自动梯度计算法)。在这种情况下,梯度不是通过反向传播算法的显式实现来计算的。 相反,它将特殊节点添加到计算图中,然后根据计算链规则计算该计算图每个算子的梯度。 为了进行反向传播运算,然后必须通过图评估引擎简单地像任何其它节点那样执行这些节点。 因此,此方法不会求导,而仅计算这些值的符号句柄。

当Tensorflow需要计算一个特定节点v相对其它节点α的梯度时,它会从v到α反向遍历图。在遍历中遇到 的每一个算子o表示依赖α的函数和产生输出张量连接集合(ν◦...◦o◦...)(α)的一个连接。因此,Tensorflow为每一个选取前一个连接梯度,并且乘以它自己梯度的算子o添加梯度节点。在遍历的最后,有一个节点带有符号句柄表示目标倒数dv/dα,即隐含实现了反向传播算法。现在可以很清楚的看到,在符号到符号方法仅仅是另一种没有任何例外的算子。下图展示了一张计算图在梯度节点增加前后的样子。

翻译 Tensorflow之旅

在文献【8】中提到符号到符号导数法可能产生相当可观的计算开销,包括内存的开销。究其原因,需要理解两组链式规则的等价公式。第一组公式会重用之前计算结果需要为了前向传播的需要,保存相应的结果。对于等式1中的任意函数f,g,h:df/dw=f′(y)·g′(x)·h′(w)withy=g(x),x=h(w) (1)

对于计算链式规则的第二种可能公式已经表明,每个函数需要重新计算它们所有的参数并且调用它们依赖的每一个函数。该公式表示成:df/dw=f′(g(h(w)))·g′(h(w))·h′(w) (2)

根据文献【8】,Tensorflow当前使用第一组公式。假定如果不采用这种方法,并且考虑到这个连接可能包括数百或数千个操作,则对于此连接的几乎每个连接必须重新计算最内层函数,因此这种选择看起来是合理的。 然而,从另一方面来看,在存储器中长时间保存张量也不是最佳的,尤其是在诸如GPU的设备上,其存储器资源是稀缺的。 对于等式2,由张量保持的存储器在理论上可以在其已被其图依赖性处理时被释放。 因此,根据文献[8]的说法,TensorFlow的开发团队称,重新计算某些张量而不是将它们保留在内存中的做法可能是未来进行性能改进的方向。

2)控制流:某些机器学习算法会从执行流控制中受益,即只在特定条件下执行特定流,或者仅仅执行固定次数。对此,Tensorflow提供了一组流控制的基本体,包括if条件跳转和loop循环。循环条件控制的存在是因为Tensorflow计算图可能是循环的。如果对于循环的次数是已知或者固定的,那么循环体可以被展开成非循环的计算序列【5】。但是为了支持可变迭代,Tensorflow需要强制跳转到如文献【8】描述的一组环运算。

当引入控制流需要特别注意的是在有反向传播的场景。 在处理条件控制的情况下,如果if操作返回一个或另一个张量,则必须知道在向前传播期间节点采取哪个分支,使得梯度节点仅被添加到该分支。 此外,当循环体(其可以是小图)被执行特定次数时,梯度计算不仅需要知道执行的迭代的次数,而且还需要访问所产生的每个中间值。 这种通过反向循环计算梯度的技术在文献[5]中称为反向传播时间。

3)Checkpoints(检查点):Tensorflow另一个基本编程模型的扩展就是检查点技术的应用。检查点技术允许模型参数可以被序列化到文件,以备再次运行模型的时候可以从文件中恢复模型参数和运行变量。因此可以在计算图中增加Save节点去保存相应变量的张量。当然,一个变量也可能连接到restore算子去恢复相应的张量。这种方法对于那些需要长时间才能训练的模型,对于模型计算容错性考量都是很好的技术,特别是在分布式环境【8】。

四. Tensorflow的编程接口

在讨论了Tensorflow计算模型之后,我们现在聚焦到更为实际的编程接口。我们先介绍可用的编程语言接口,再使用一个例子讲解Python API的使用。最后,我们概括Tensorflow API的格局和怎么能够快速创建算法原型。

A. 接口

Tensorflow拥有C++和Python两种编程接口,允许用户调用后端功能。Python API提供了丰富和完整的创建和运行计算图的编程接口,而C++的接口相对有限和与后端功能实现耦合度太高,仅仅允许去执行计算图和序列化图到谷歌协议缓冲格式。对于创建图的C++接口在文章发表的时候还很不完善。

值得注意的是,Tensorflow的API和NumPy有很好的集成。因此,我们可以看到Tensorflow的数据类型tensor和NumPy的ndarrays在很多应用场合都是可以互换的。

B. 示例解读

接下来的几个章节,我们将一步一步的解读一个Tensorflow的真实示例。我们将训练一个带有一个输入和一个输出的简单的多层感知器(MLP),它将用于识别MNIST数据集中的手写字符。在这个数据集中,样本时28x28像素的手写字符从0-9。我们将这些字符转换成784灰度像素扁平的向量。对于每个样本的标签是相应字符的数字。

我们从加载输入数据开始解读。这里数据已经被处理好成为我们需要的格式,只需要调用read函数加载数据到内存。进一步来看,我们设置one_hot=True来指定是否使用一个10维向量(d1,。。。,d10)的转置来表征某一个字符,即所有维度数字是0,只有表征该字符的位置是1。

importtensorflow as tf

# Download and extract the MNIST data set.

# Retrieve the labels as one-hot-encoded vectors.

mnist = mnist_data.read('/tmp/mnist', one_hot=True)

接下来,我们通过调用tf.Graph创建一个新计算图。为了给这个图增加算子,我们必须把这个图注册为缺省图。在TensorflowAPI和库设计里面,新的算子总是挂在缺省图上。相应代码如下:

# Create a new graph

graph = tf.Graph()

# Register the graph as the default one to add nodes

with graph.as_default():

# Add operations ...

我们现在准备通过增加算子来生成计算图。让我们先增加两个占位节点examples和labels。占位者是一种特殊变量,在图运算的时候必须被确切的张量赋值。也就是说,上面创建的占位者必须在Session.run()被调用的时候,被feed_dict参数传进来的张量所取代。对于每一个这个的占位者,我们定义它的形状和数据类型。Tensorflow在这里可以使用None来描述占位者形状的第一个维度。这就是为未来给此占位者赋值一个在此维度大小可变的张量。对于examples的列大小,我们指定每一幅图的特征数,即28 x 28 = 784个像素。labels占位者应该有10列,代表着我们在上面定义的10维字符分类向量。

# Using a 32-bit floating-point data type tf.float32

examples = tf.placeholder(tf.float32, [None, 784])

labels = tf.placeholder(tf.float32, [None, 10])

给定一个example矩阵X属于集合R是nx784,意味着包含n个图像,学习算法将使用仿射变换X.W+b,这里W是一个784x10的权重矩阵,b是10维偏置向量。这个变换的结果产生Y是nx10的矩阵,包含我们模型对于每一个样本图像识别的结果。这些结果是一些任意数值而不是概率分布。为了转换它们到一个有效概率分布,在给定似然Pr[x=i],即第x样本图像是数字i的概率,我们使用softmax函数,公式和相应代码如下:

# Draw random weights for symmetry breaking

weights = tf.Variable(tf.random_uniform([784, 10]))

# Slightly positive initial bias

bias = tf.Variable(tf.constant(0.1, shape=[10]))

# tf.matmul performs the matrix multiplication XW

# Note how the + operator is overloaded for tensors

logits = tf.matmul(examples, weights) + bias

# Applies the operation element-wise on tensors

estimates = tf.nn.softmax(logits)

翻译 Tensorflow之旅

接下来,我们计算我们的目标函数,产生模型在当前权重W和偏差b下的成本或者损失。公式H(L,Y)i=−jLi,j·log(Yi,j)计算我们预测的结果和训练样本实际结果之间的交叉熵。更精确而言,我们考虑的是所有训练样本的交叉熵的均值作为成本或者损失。

# Computes the cross-entropy and sums the rows

cross_entropy = -tf.reduce_sum(labels*tf.log(estimates), [1])

loss = tf.reduce_mean(cross_entropy)

现在,我们有了目标函数,就可以进行随机梯度下降计算去更新我们模型的权重矩阵。为此,Tensorflow提供了GradientDescentOptimizer这个类实现这一过程。该类使用算法的学习速度来初始化,同时提供算子minimize来处理我们的成本或损失张量。这就是我们在Session环境里训练模型需要反复迭代的算子。

# We choose a learning rate of 0.5

gdo = tf.train.GradientDescentOptimizer(0.5)

optimizer = gdo.minimize(loss)

最后,我们就可以实际训练模型了。在Tensorflow里,我们需要进入一个会话环境,使用tf.Session来管理会话。通过Session,我们训练我们的模型,执行计算图上的算子。我们有几种调用方法,最常见的方式是调用Session.run(),然后传递一组张量给它。或者,我们也可以直接在张量上调用eval()和算子上run()。在评估算子之前,我们必须确保我们图变量被初始化了。理论上,我们可以通过调用Variable.initializer算子来初始化每一个变量。然而,最常见的办法是调用tf.initialize_all_variables()方法去一次性初始化所有变量。然后我们就可以迭代几次随机梯度下降,每一次我们选取一些样本图像和标签,传递给模型去计算成本或损失。最后,我们的成本或损失会越来越小(我们希望如此):

with tf.Session(graph=graph) as session:

# Execute the operation directly

tf.initialize_all_variables().run()forstepinrange(1000):

# Fetch next 100 examples and labels

x, y = mnist.train.next_batch(100)

# Ignore the result of the optimizer (None)

_, loss_value = session.run(

[optimizer, loss],

feed_dict={examples: x, labels: y})

print(’Loss at step {0}: {1}’.format(step, loss_value))

这个例子完整的代码参见附录。

C. 抽象与封装

你可以已经注意到构建模型和训练的过程是需要花费大量的开销在创建一个非常简单的两层网络。通过深度学习的名称“深度”二字,就隐含了,你需要使用携带大量隐藏层的深度神经网络。因此,每一次构建模型都需要花这么多时间构建权重矩阵,偏置矩阵,计算矩阵乘法,加法,和应用非线性激活函数的神经网络是效率低下的,因此,我们需要抽象和封装这一过程。幸好,我们可以使用一些开源的库来完成这一过程,比如PrettyTensor,TFLearn和Keras。下面我们会分别论述PrettyTensor和TFLearn。

1)PrettyTensor:这是谷歌开发的高级编程接口,主要的是通过Builder模式来调用Tensorflow的API。它允许用户把Tensorflow的算子和张量封装进干净的版本,然后迅速将任意层串接起来。比如,它可以用一行代码完成一个输入张量传输到一个全连接(稠密)神经网络层。下面的样例代码显示了从创建一个占位者到初始化,到创建三层网络到最后输出softmax的一个分布。

examples = tf.placeholder([None, 784], tf.float32)

softmax = (prettytensor.wrap(examples)

.fully_connected(256, tf.nn.relu)

.fully_connected(128, tf.sigmoid)

.fully_connected(64, tf.tanh)

.softmax(10))

2)TFLearn:这是另一个基于Tensorflow本体的封装库,它有高度封装的神经网络层和层连接API。而且,它比PrettyTensor一定要通过Session建立训练和评估模型更进一步,它可以直接添加训练样本和标签来训练模型。在TFLearn里面,有创建完整神经层次的函数,有返回原始Tensorflow对象的函数,有混合Tensorflow本体代码调用的编程。比如,我们可以取代Tensorflow的输出层,自己完整构建它,但是依然保持其它部分不发生改变。下面10行代码完成了附录65行代码才能完成的功能。

importtflearn

importtflearn.datasets.mnist as mnist

X, Y, validX, validY = mnist.load_data(one_hot=True)

# Building our neural network

input_layer = tflearn.input_data(shape=[None, 784])

output_layer = tflearn.fully_connected(input_layer,

10, activation=’softmax’)

# Optimization

sgd = tflearn.SGD(learning_rate=0.5)

net = tflearn.regression(output_layer,

optimizer=sgd)

# Training

model = tflearn.DNN(net)

model.fit(X, Y, validation_set=(validX, validY))

五. Tensorflow图的可视化

深度学习模型通常使用错综复杂的神经网络。比如,文献【19】描述的谷歌Inception模型就是一个有36000个独立单元的卷积神经网络。而文献【8】描述的某种长短时记忆模型(LSTM)用到了15000个节点。为了使用和调优如此复杂的网络,必须依赖强有力的可视化工具。Tensorboard是Tensorflow的可视化仪表盘。本章节将讲述Tensorboard的功能。

A. Tensorboard的特性

Tensorboard的核心特性就是构建明细易懂计算图的可视化界面。如下图例子可以看到Tensorboard是如何展示计算图的。

翻译 Tensorflow之旅

name scope(命名空间)是Tensorflow里面一个重要可视化分类方法。它可以把同属于一个命名空间的算子,关系,输入和输出显示在同一个方框图内。下图展示了,从上图扩展一个layer1命名空间的详细算法细节。

翻译 Tensorflow之旅

另外,Tensorboard允许用户追踪单个变量在训练过程中的变化情况。你可以附加两类总结算子到计算图上去生成报告(scalar summaries和histogram summaries)。Scalar summaries显示的是张量伸缩的进度图,也就是在某次训练迭代的采样数据。这样你可以看到训练的精确度和损失变化情况。Histogram summaries节点允许用户去追踪数值分布情况,比如神经网络的权重或者softmax估值分布。下图展示了这两种报告。

翻译 Tensorflow之旅

我们注意到Tensorboard采用了网页交互形式。一旦你的计算图被加载进来,你就可以观察模型,监控算子运行。具体的Tensorboard在线演示可以在这里找到:https://www./tensorboard/index.html

B. Tensorboard的实操

为了把Tensorboard整合到你的Tensorflow代码,你需要至少做三步。第一,你需要使用命名空间来规划你的节点。第二,你需要给你的算子增加某种类型的报告。最后第三部,你需要调用SummaryWriter去把Summaries得到的张量写到文件中去。与其分别去写每一个summaries,还不如调用tf.merge_all_summaries()整合所有的summaries,然后一次性写入文件。下面展示了Tensorboard的样例代码。

with tf.name_scope(’Variables’):

x = tf.constant(1.0)

y = tf.constant(2.0)

tf.scalar_summary(’z’, x + y)

merged = tf.merge_all_summaries()

writer = tf.train.SummaryWriter(’/tmp/log’, graph)

with tf.Session(graph=graph):forstepinrange(1000):

writer.add_summary(

merged.eval(), global_step=step)

六. 比较其它深度学习框架

除了Tensorflow,我们还能找到其它一些深度学习框架。人气比较高的有Theano,Torch和Caffe。在这一章,我们会探索这些框架和Tensorflow的异同点,进行一系列定性和定量的分析。

A. 定性分析

下面我们分别比较上述三种深度学习框架和Tensorflow的差异。下图则是汇总了这个比较。

翻译 Tensorflow之旅

1)Theano:在我们需要讨论的三种Tensorflow的替代框架中,Theano是最像Tensorflow的。如同Tensorflow一样,Theano的编程模型也是声明式而不是命令式的基于计算图。而且,Theano也使用符号差异化。然后Theano有一个比较长时间的图编译时间,因为它需要把Python代码转化为C++/CUDA代码【5】。一方面,这是因为Theano采用了很多高级图优化的算法【5】,而Tensorflow仅仅是做了子图消减优化。对比Tensorboard, Theano的可视化工具可以用可怜来形容。它除了输出一些可读的文字来表示图或静态图像,它还需要一个插件来生成类似交互式网页可视化,剩下来的东西,Theano就是泛善可陈了。

2)Torch:Torch和Tensorflow根本不同的地方是Torch是C/CUDAde后端实现,加上Lua的前端编程接口。Lua确实是相当快的脚本语言,可以快速构建原型系统,但是相对于Python,它算是非主流。虽然Lua 具备各种性能和功能上的优势,但是对于工业界而言,对比Tensorflow的Python API而言,就显得曲高和寡了。除了编程语言的考量,Torch的编程模型也和Tensorflow不同。它采用命令式编程,也不声明计算图。这就要求程序员还要仔细安排算子执行的顺序。这也暗示了Torch在前向和反向传播计算梯度的时候,是用符号到数值而不是符号到符号差异化来优化。

3)Caffe:Caffe和Tensorflow有着天壤之别。Caffe的模型创建拥有MATLAB和Python编程接口,主要使用谷歌的协议缓冲语言,这样带来和Python截然不同的编程体验。另外,在Caffe里面的基本构建单元是神经网络的层,而不是Tensorflow里面的算子。这样Tensorflow算是更为底层的框架。和Torch相似,Caffe也不是专注于构建计算图,或者符号。所以计算导数是通过符号到数值的方法。Caffe特别适合于开发卷积神经网络,用于图像识别任务。但是,它就没有Tensorflow那样在神经网络领域具备普适性。比如Caffe在架构上不支持循环架构,而这时RNN,LSTM这样神经网络的基本构造。另外,Caffe也不支持分布式系统。

B. 定量分析

接下来,我们会对这几种框架做定量分析来,同时给出深度学习框架评估的整体发展趋势。

文献【20】的研究是由博世研发中心在2016年3月进行的,他们对比了Tensorflow,Torch,Theano和Caffe这几种不同的神经网络架构。他们在Intel Xeon E5-1650 v2 CPU @ 3.50 GHz and an NVIDIA GeForce GTX Titan X/PCIe/SSE2 GPU这样配置的机器上安装Ubuntu14.04,然后跑LeNet CNN模型【21】来看不同的框架的性能如何。他们特别留意神经网络的前向传播速度,因为他们相信这和框架部署策略有关;还有反向传播速度,这是和训练模型性能相关。我们摘要了他们结论如下表,这个结果是在两种情况下得到的,(a)是一个CPU跑12个线程,(b)是GPU。从结果来看,有趣的是Tensorflow无论是在CPU还是在GPU都跑不过Torch。最有意思的是Tensorflow在跑GPU的时候成绩掉的很厉害,是倒数第一。文献【20】的作者注意到这可能是由于测试Tensorflow使用的是NVIDIA的cuDNNv2,而其他库用的是v3. 他们在文中强调,这么做是因为Tensorflow的官方文档建议这么配置cuDNN的版本。

翻译 Tensorflow之旅

我们能够拿到的第二个测试来源是文献【22】卷积神经网络评测平台的结果,你可以在github上找到他们。它是由Facebook的一个AI研究工程师Soumith Chintala维护的。我们参考的结果是2016年五月25日提交的。Chintala提交了围绕卷积神经网络的许多框架的实现,当然包括Tensorflow,Torch和Caffe。Theano没有参加评测,所以我们这里看不到它的有关数据。该作者声称的硬件配置是6-core Intel Core i7-5930K CPU @ 3.50GHz配有an NVIDIA Titan X graphics chip,跑的是Ubuntu14.04。该评测也分别给出了正向传播和反向传播的速度如下表:

翻译 Tensorflow之旅

不出意料,Tensorflow是仅次于Torch的框架。最后我们来看一下文献【5】的评测,这是由Theano开发团队在2016年5月9日提交的报告。除了CNN模型之外,他们还测试了之前我们提到的AlexNet架构,测试还包括了在Penn Treebank【24】上跑LSTM的结果。他们的评测针对小模型(200个节点的单隐藏层,每个序列长20),统计每秒处理多少单词;针对大模型(两个650个节点的隐藏层,序列长为50)。在文献【5】还提到一个中等模型的测试,这里我们不做讨论,评测结果如下图所示:

翻译 Tensorflow之旅

这个结果是在硬件配置NVIDIA Digits DevBox with 4 Titan X GPUs and an Intel Core i7-5930K CPU的机器上跑出来的。,而且他们给所有框架使用的都是cuDNN v4。Caffe的运行结果没有在列。在他们的评测结果里,Tensorflow在小模型表现最好,大模型稍差。这个表格来源于文献【5】。

当Tensorflow刚刚发布的时候,表现奇差,整个深度学习社区都表示失望。随后,不断推出的新版本,不断改进和增强功能,给深度学习带来了惊人的进步。这也反映在我们的一系列挑选工作中。最早的三次评测【20】表明,Tensorflow完全不能和Torch,Theano和Caffe相提并论。但是仅仅两个月后,【22】的结果已经显示Tensorflow追赶上来,到了最后的评测【5】的时候,Tensorflow已经处在领先的地位了。我们预测Tensorflow的性能还会继续改进,前途无量。特别是在分布式处理的场景下,目前和可以预见的将来,Tensorflow都将是领先地位。

七. Tensorflow的实际应用

Tensorflow发布才短短六个月,学术界和工业界还没有能够完全拥抱Tensorflow,一些现有应用的移植还需要时间,新的研究还需时日。但是一点毋庸置疑,谷歌已经积极应用和推广Tensorflow在各种任务中【19】【25】【26】【27】【28】。我们将选择性的看一看这些应用是怎么使用Tensorflow的。

A. 学术界

首先提到Tensorflow的是2016年二月的文献【29】,来自谷歌大脑组的Szegedy,Ioffe和Vanhoucke发表的文章。他们用Tensorflow改进了Inception模型【19】,主要用于图像识别。作者给出了ImageNet测试集最高5档3.08%偏差的好成绩。

在文献【25】,Ramsunder等人对于药品发现的多任务网络的讨论使用了Tensorflow,这是斯坦福大学和谷歌之间的联合研究工作。这篇文章描述了使用Tensorflow构建深层神经网络做虚拟扫描来选择潜在的候选药物。这是为了帮助医药公司和科研社区寻找为治疗疾病的新药。

August和Ni应用Tensorflow创建了递归神经网络优化动态解耦,一种在量子内存抑制误差的技术。【30】作者旨在维持量子纠缠态,这是构建通用量子计算机的关键需求。

最后,文献【31】研究了自然语言处理的sequence-to-sequence模型,作者使用Tensorflow,采用滑动窗口技术去做字符级别的英语到拉脱维亚语在音频和视频内容的翻译。作者用这个方法去分类电视节目,广播节目,并且聚类单个的故事。

B. 工业界

除了谷歌之外,工业界还很少有人使用Tensorflow,至少公开的情况来看是这个样子。所以我们来看看谷歌的Tensorflow应用情况。

最近,谷歌在调整它的核心业务算法PageRank【32】系统RankBrain【33】,使用的技术就是Tensorflow。RankBrain使用大规模分布式深度神经网络来做搜索排序。根据文献【33】,超过15%发给www.google.com的查询是全新查询。RankBrain系统可以通过相似性比对来给出未知查询建议。

另一个应用是谷歌的智能邮件回复系统【27】。谷歌已经整合了智能回复功能在其邮件系统的Inbox里面。系统使用递归神经网络和特定LSTM模块进行序列到序列的学习和自然语言理解。一个编码器映射一个语料库到一个“思维向量”,而解码器则在语法和语义上合成正确的回复,然后以建议的形式呈现给用户。

在文献【26】,谷歌在使用卷积神经网络做图像识别和自动文本翻译。作为谷歌手机的内置功能,对于用户来说的外文可以被算法识别,然后翻译成用户识别语言,并且呈现在原文所在图像上。这种方法可以用来翻译图片里的街道名。文献【26】特别强调如果部署这种算法在低端手机设备和慢速网络上。因此,神经网络训练小模型,然后应用于资源首先的场景也在发展。

最后,我们已经注意到谷歌旗下的DeppMind,著名的AI研究机构已经把他们的研究平台从Torch7转移到Tensorflow了。【28】有消息称【17】,DeepMind使用Tensorflow,在谷歌新发布的张量处理单元(TPU)上训练他们的AlphaGo模型。该文献作者就是谷歌DeepMind的雇员,揭示了为什么Tensorflow对于DeepMind有好处的四个理由:

1)Tensorflow的大规模应用是构建在谷歌云平台上,可以很容易提供充足的计算力。

2)Tensorflow支持TPU这样的硬件扩展。

3)Tensorflow的主要编程接口是Python,这是谷歌的核心编程语言,要比Lua能够得到更好的支持。

4)Tensorflow是能够很好支持多GPU的框架。

八. 总结

我们已经从各个方面完整的讨论了Tensorflow,这种基于计算图的开源深度学习库的各种特性,包括能够快速计算梯度,它固有的支持分布式计算的特性,强有力可视化工具。它能够在很细颗粒度构建神经网络,允许高度定制模型,同时也支持一些快速原型搭建的封装库,比如TFLearn。相比Torch,Theano之类的框架,Tensorflow增加了新特征和改进了现有特性。它在性能方面的表现,开始不尽人意,但是随着时间的推移,不断的随着新库的发布在改进。

我们注意到对于Tensorflow的分布式运行性能的评测目前开展的很少。我们认为这是很重要的一环,也是学术界需要深入研究的一点。

Tensorflow已经在开源社区取得了相当的人气和强有力的第三方支持。谷歌已经做出了明智的决定。我们相信,Tensorflow不仅仅对于它的所有者有利,也会惠及更为广大的科研社区;它会打开通往更快更大规模的人工智能之门。

附录-1

#!/usr/bin/env python

# -*- coding: utf-8 -*-

''' A one-hidden-layer-MLP MNIST-classifier. '''

from__future__importabsolute_importfrom__future__importdivisionfrom__future__importprint_function

# Import the training data (MNIST)

fromtensorflow.examples.tutorials.mnistimportinput_data

importtensorflow as tf

# Possibly download and extract the MNIST data set.

# Retrieve the labels as one-hot-encoded vectors.mnist = input_data.read_data_sets('/tmp/mnist',

one_hot=True)

# Create a new graph

graph = tf.Graph()

# Set our graph as the one to add nodes to

with graph.as_default():

# Placeholder for input examples (None =

variable dimension)

examples = tf.placeholder(shape=[None, 784],

dtype=tf.float32)

# Placeholder for labels

labels = tf.placeholder(shape=[None, 10],

dtype=tf.float32)

weights =

tf.Variable(tf.truncated_normal(shape=[784,

10], stddev=0.1))

bias = tf.Variable(tf.constant(0.1, shape=[10]))

# Apply an affine transformation to the input

features

logits = tf.matmul(examples, weights) + bias

estimates = tf.nn.softmax(logits)

# Compute the cross-entropy

cross_entropy = -tf.reduce_sum(labels*tf.log(estimates),

reduction_indices=[1])

loss = tf.reduce_mean(cross_entropy)

# Create a gradient-descent optimizer that

minimizes the loss.

# We choose a learning rate of 0.01

optimizer =

tf.train.GradientDescentOptimizer(0.5).minimize(loss)

# Find the indices where the predictions were

correct

correct_predictions = tf.equal(

tf.argmax(estimates, dimension=1),

tf.argmax(labels, dimension=1))

accuracy =

tf.reduce_mean(tf.cast(correct_predictions,

tf.float32))

with tf.Session(graph=graph) as session:

tf.initialize_all_variables().run()forstepinrange(1001):

# And finally the loss

example_batch, label_batch =

mnist.train.next_batch(100)

feed_dict = {examples: example_batch, labels:

label_batch}

ifstep % 100 == 0:

_, loss_value, accuracy_value =

session.run(

[optimizer, loss, accuracy],

feed_dict=feed_dict

)print('Loss at time {0}: {1}'.format(step,

loss_value))print('Accuracy at time {0}:

{1}'.format(step, accuracy_value))

optimizer.run(feed_dict)

参考文献

翻译 Tensorflow之旅

翻译 Tensorflow之旅

翻译 Tensorflow之旅

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多