2.1 深度学习框架简介
随着深度学习技术的不断发展,近年来各种深度学习框架层出不穷。不同的深度学习框架拥有的功能也千差万别。虽然深度学习框架的种类很多,每个种类又有不同的功能,但是从本质上说,所有的深度学习框架都要支持一些基本的功能,并且通过组合这些功能,有能力构建复杂的神经网络模型。下面介绍一下深度学习框架需要支持的功能。
2.1.1 深度学习框架中的张量
一个深度学习框架首先需要支持的功能是张量的定义。在第1章我们可以看到,张量的运算是深度学习的核心,比如,一个迷你批次大小的图片可以看成四维的张量,一个迷你批次的文本可以看成二维张量等。基本上所有深度学习模型的神经网络层(包括线性变换层和激活函数层等)都可以表示为张量的操作,梯度和反向传播算法也可以表示成张量和张量的运算。前面已经介绍过张量在内存中的排列方式是连续的,而且是从最后一个维度开始排列。在深度学习框架中,张量首先应该能够支持得到每一个维度的长度,也就是保存张量的形状。其次,需要能够支持和其他相同类型张量的运算,如加、减、乘、除等。另外,张量还应该能够支持变形,也就是说,变换成总元素的数目相同,但是维数不同的另外一个张量。比如,可以把某个维度分裂成两个维度,或者把最后几个维度合并成一个维度等。由于变形前后两个张量的分量在内存中的排列形状一般是相同的,因此,这两个张量一般会在底层共享同一个内存区域。最后,张量应该能支持线性变换的操作(如卷积层和全连接层运算)和对每个元素的激活函数操作。实现线性变换操作和激活函数操作相关的类和函数是为了能够方便地进行神经网络的前向计算。
2.1.2 深度学习框架中的计算图
有了张量的定义和运算还不够,我们需要让一个深度学习框架支持反向传播计算和梯度计算。为了能够计算权重梯度和数据梯度,一般来说,神经网络需要记录运算的过程,并构建出计算图。关于构建计算图的方法,深度学习框架采用的是两种策略。第一种是静态图(Static Graph),也就是在计算之前预先构建一个计算图,图中的节点是运算操作(Operator),包括矩阵乘法和加减等,而图中的边则是参与运算的张量。支持静态计算图的框架包括TensorFlow和Caffe等。在使用框架时,一般使用配置文件或者不同语言的接口函数先构建出深度学习模型对应的静态图,然后在静态图中输入张量,框架的执行引擎会根据输入的张量进行计算,最后输出深度学习模型的计算结果。静态图的前向和反向传播路径在计算前已经被构建,所以是已知的。由于计算图在实际发生计算之前已经存在,执行引擎可以在计算之前对计算图进行优化,比如删除冗余的运算合并两个运算操作等。同时因为计算图是固定的,而不是每次计算都重新构建计算图,这有效地减少了计算图构建的时间消耗,相对来说效率较高。但静态图也有一定的缺点,主要是因为静态图为了效率牺牲了灵活性。因为静态计算图在构建完成之后不能修改,因此,在静态计算图中使用条件控制(比如循环和判断语句)会不大方便。首先,计算图需要有对应条件控制的计算操作。另外,如果在计算图中使用条件控制,一般需要计算条件的所有不同分支,也会给计算图增加一定的消耗。其次是框架的易用性,因为静态计算图在构建时只能检查一些静态的参数,比如输入/输出张量的形状是否合理,在执行时暴露的一些问题可能就无法在图的构建阶段预先排查,只能在引擎执行的时候被发现。因此,在代码的调试方面,静态计算图相对来说比较低效,同时也造成了基于静态计算图的深度学习框架上手比较困难。相比于静态计算图,动态计算图牺牲了执行效率,但灵活性更强。
总结一下上面的叙述,动态计算图指的是在计算过程中逐渐构建计算图,最后得到神经网络的输出结果。如果要进行反向传播的计算,动态计算图的反向传播路径只有在计算图构建完成的时候才能获得。使用动态计算图的深度学习框架有PyTorch和Chainer等。对动态图来说,条件控制语句非常简单,可以使用前端语言(如Python等)现成的条件控制语句。动态计算图的另一个特点是使用方便,因为可以实时输出深度学习模型的中间张量。因此,调试起来非常方便,通过检查程序出错处的张量的值,可以很容易得到出错的原因,相对于静态图需要不断修改图的构建过程并重新执行新构建的图,动态图的调试过程非常方便。顺便提一句,动态图的构建和计算是深度学习框架的主流发展方向,类似TensorFlow之类的传统的基于静态图的深度学习框架也开始开发支持动态图计算的模块。
2.1.3 深度学习框架中的自动求导和反向传播
除了计算图的构建,深度学习框架的另一个特性是能够进行自动求导(Automatic Differentiation)。一般深度学习模型在训练的时候,需要构建计算图,计算图的最后输出是一个损失函数的标量值,然后根据1.6.1节介绍的反向传播算法,从标量值反推计算图权重张量的梯度,这个过程被称为自动求导。在静态图的计算过程中,在构建前向的计算操作的同时会构建一个反向传播的梯度计算操作,这样,前向的计算图的构建完毕伴随着反向计算图的构建完毕。有了损失函数之后,就可以从损失函数所在的张量的边开始逐渐沿着反向计算图获取对应的梯度。与静态计算图的构建相比,动态计算图在构建前向计算图的时候则是给每个输出张量绑定一个反向传播的梯度计算函数,当计算图到达最终的损失函数张量的时候,直接调用该张量对应的反向传播函数,并不断根据前向计算图进行递归的反向传播函数调用,最后到达输入张量,即可求得每个权重张量对应的梯度张量。
在损失函数的优化方面,基于静态计算图的深度学习框架直接在计算图中集成了优化器,在求出权重张量梯度之后直接执行优化器的计算图,更新权重张量的值。与静态计算图相比,动态计算图则直接把优化器绑定在了权重张量上。当反向传播计算完毕之后,权重张量会直接绑定对应的梯度张量,优化器的工作则是根据绑定的梯度张量来更新权重张量。同样,与静态计算图相比,动态计算图在损失函数的优化方面可以更加灵活地进行设计,比如,可以很方便地指定一些权重使用一个学习率,而另外一些权重使用另外一个学习率。
作为一个基于Python的深度学习框架,PyTorch使用了动态计算图,而且在API的设计上类似于Numpy等流行的Python数值计算框架,非常方便初学者上手。在保持API的简单性的同时,PyTorch还提供了强大的可扩展性,包括能够自由定制张量计算操作、CPU/GPU上的异构计算,以及并行训练环境等。本章主要介绍的是PyTorch的一些基本数值操作。在后续章节中将会介绍PyTorch的各种高级特性。