维基百科 [1] 列举了近20种的深度学习框架,有许多已经逐渐被淘汰了,以Python/C++语言的框架而言,目前比较流行的只剩以下四种,具体见表3.1。
表3.1 Python/C++语言框架
TensorFlow、PyTorch分居占有率的前两名,其他框架均望尘莫及,如图3.1所示。因此推荐读者若是使用Python语言,熟悉TensorFlow、PyTorch就绰绰有余了;若偏好C/C++,则可以使用Caffe框架。
图3.1 深度学习框架在Arxiv网站的论文采用比率
(图片来源: Which deep learning framework is the best? [2] )
本书的范例以TensorFlow/Keras为主,不会涉及其他深度学习框架,以期对TensorFlow/Keras进行全面而深入的探讨。
梯度下降法是神经网络主要求解的方法,计算过程需要使用大量的张量运算,另外,在反向传导的过程中,要进行偏微分,并需解决多层结构的神经网络问题。因此,大多数的深度学习框架至少都会具备以下功能。
(1)张量运算:包括各种向量、矩阵运算。
(2)自动微分。
(3)神经网络及各种神经层(Layers)。
学习的路径可以从简单的张量运算开始,再逐渐熟悉高阶的神经层函数,以奠定扎实的基础。
接着再来了解TensorFlow执行环境(Runtime),如图3.2所示。它支持GPU、分布式计算、低阶C API等,也支持多种网络协议。
图3.2 TensorFlow执行环境(Runtime)
(图片来源:TensorFlow GitHub [3] )
TensorFlow执行环境包括以下三个版本。
(1)TensorFlow:一般计算机版本。
(2)TensorFlowjs:网页版本,适合边缘运算的装置及虚拟机装置(Docker/Kubernetes)。
(3)TensorFlow Lite:轻量版,适合指令周期及内存有限的移动设备和物联网装置。
程序设计堆栈(Programming Stack)如图3.3所示,在2.x版后以Keras为主轴,TensorFlow团队依照Keras的规格重新开发,逐步整合其他模块,比独立开发的Keras框架功能更加强大,因而Keras“大神”François Chollet也宣告Keras独立框架不再升级,甚至将Keras官网也改为介绍TensorFlow的Keras模块,而非自家开发的Keras独立框架。所以,现在要撰写Keras程序,应以TensorFlow为主,避免再使用Keras独立框架。
图3.3 TensorFlow程序设计堆栈(Programming Stack)
(图片来源: Explained : Deep Learning in Tensorflow [4] )
TensorFlow Keras模块逐步整合其他模块及工具,如果读者之前使用的是Keras独立框架,则可以补强以下项目。
(1)低阶梯度下降的训练(GradientTape)。
(2)数据集载入(Dataset and Loader)。
(3)回调函数(Callback)。
(4)估计器(Estimator)。
(5)预训模型(Keras Application)。
(6)TensorFlow Hub:进阶的预训模型。
(7)TensorBoard可视化工具。
(8)TensorFlow Serving部署工具。
我们会在后面的章节陆续探讨各项功能。
除了Keras的引进,特别要注意的是, TensorFlow 2.x版之后,默认模式—修改为Eager Execution Mode ,舍弃了Session语法,若未调回原先的Graph Execution Mode,则1.x版的程序执行时将会产生错误。由于目前网络上许多范例的程序新旧杂陈,还有很多1.x版的程序,读者应特别注意。
TensorFlow顾名思义就是提供张量的定义、计算与变量值的传递功能。因此,它最底层的功能即支持各项张量的数据类型与其运算函数,基本上都遵循NumPy库的设计理念与语法,包括传播(Broadcasting)机制的支持。
以下我们直接以实践代替长篇大论,读者请参阅 03_3_张量运算.ipynb 。
(1)显示TensorFlow版本。程序代码如下:
(2)检查GPU是否存在。程序代码如下:
执行结果如下,可显示GPU简略信息:
[PhysicalDevice(name='/physical_device:GPU:0',device_type='GPU')]
(3)如果要知道GPU的详细规格,可安装PyCuda模块。请注意在Windows环境下,无法以pip install pycuda安装,可到https://www.lfd.uci.edu/~gohlke/pythonlibs/?cm_mc_uid=08085305845514542921829&cm_mc_sid_50200000=1456395916#pycuda下载对应Python、Cuda Toolkit版本的二进制文件。
举例来说,Python v3.8且安装Cuda Toolkit v10.1,则须下载pycuda-2020.1+cuda101-cp38-cp38-win_amd64.whl,并执行pip install pycuda-2020.1+cuda101-cp38-cp38-win_amd64.whl。接着就可以执行本书所附的范例:python GpuQuery.py。
执行结果如下,即可显示GPU的详细规格,重要信息排列在前面。
(4)声明常数(constant),参数可以是常数、list、NumPy array。程序代码如下:
(5)支持四则运算。程序示例如下:
执行结果如下:
tf.Tensor([[11 12]], shape=(1, 2), dtype=int32)
tf.Tensor([[-9 -8]], shape=(1, 2), dtype=int32)
tf.Tensor([[2 4]], shape=(1, 2), dtype=int32)
tf.Tensor([[0.5 1. ]], shape=(1, 2), dtype=float64)
注意:如果要显示数值,须转换为NumPy array。例如:
(x+10).numpy()
(6)四则运算也可以使用TensorFlow函数。程序代码如下:
reduce_sum是沿着特定轴加总,输出会少一维,故[1, 2, 3]套用函数运算后等于6。
(7)TensorFlow会自动决定在CPU或GPU运算,可由下列指令侦测。一般而言,常数(tf.constant)放在CPU,其他变量则放在GPU上,两者加总时,会将常数搬到GPU上再加总,过程不需要人为操作。但请 注意,PyTorch框架稍显麻烦,必须手动将变量搬移至CPU或GPU运算,不允许一个变量在CPU,另一个变量在GPU 。侦测程序代码如下:
执行结果:
x1是否在GPU #0上:False
x2是否在GPU #0上:True
x3是否在GPU #0上:True
(8)用户也能够使用with tf.device("CPU:0")或with tf.device("GPU:0"),强制指定在CPU或GPU运算。程序代码如下:
①第一次执行结果如下,CPU运算比GPU快:
On CPU:64.00ms
On GPU:311.49ms
②多次执行后,GPU反而比CPU运算快了很多:
On CPU:58.00ms
On GPU:1.00ms
(9)稀疏矩阵(Sparse Matrix)运算:稀疏矩阵是指矩阵内只有很少数的非零元素,如果依一般的矩阵存储会非常浪费内存,运算也是如此,因为大部分项目为零,不需浪费时间计算,所以,科学家针对稀疏矩阵设计出了特殊的数据存储结构及运算算法,TensorFlow也支持此类数据类型,如图3.4所示。
图3.4 稀疏矩阵
(10)TensorFlow稀疏矩阵只需设定有值的位置和数值,并设定维度如下:
执行结果如下:
(11)转为正常的矩阵格式。程序代码如下:
执行结果如下:
(12)如果要执行TensorFlow 1.x版Graph Execution Mode的程序,则需禁用(Disable)2.x版的功能,并改变加载框架的命名空间(Namespace)。程序代码如下:
TensorFlow改良反而带来了后向兼容性差的困扰,1.x版的程序在2.x版的默认模式下均无法执行,虽然可以把2.x版的默认模式切换回1.x版的模式,但是要自行修改的地方较多,而且也缺乏未来性,因此,在这里建议大家以下几点。
①Eager Execution Mode已是TensorFlow的主流,不要再使用1.x版的Session或TFLearn等旧的架构,套用一句电视剧对白, 已经回不去了 。
②手上有许多1.x版的程序,如果很重要,非用不可,可利用官网移转(Migration)指南 [5] 进行修改,单一文件比较可行,但若是复杂的框架,可能就要花费大量时间了。
③官网也有提供指令,能够一次升级整个目录的所有程序,如下:
tf_upgrade_v2 --intree <1.x版程序目录> --outtree <输出目录>
详细使用方法可参阅TensorFlow官网升级指南 [6] 。
(13)禁用2.x版的功能后,测试1.x版程序,Graph Execution Mode程序须使用tf.Session。程序代码如下:
(14)GPU内存管理:由于TensorFlow对GPU内存的垃圾回收(Garbage Collection)机制并不完美,因此,常会出现下列GEMM错误:
此信息表示GPU内存不足,尤其是使用Jupyter Notebook时,因为Jupyter Notebook是一个网页程序,关掉某一个Notebook文件,网站仍然在执行中,所以不代表该文件的资源会被回收,通常要选择 Kernel > Restart 选项才会回收资源。另一个方法,就是限制GPU的使用配额。以下是TensorFlow 2.x版的方式,1.x版并不适用。程序代码如下:
(15)用户也可以不使用GPU。程序代码如下:
反向传导时,会更新每一层的权重,这时就会用到偏微分运算,如图3.5所示。所以,深度学习框架的第二项主要功能就是自动微分。
图3.5 神经网络权重求解过程
同样地,我们直接以实践代替长篇大论,请参阅 03_2_自动微分.ipynb 。
(1)使用tf.GradientTape()函数可自动微分,再使用g.gradient(y, x)可取得 y 对 x 作偏微分的梯度。程序代码如下:
执行结果:
f ( x )= x 2
f ′( x )= 2 x
f ′(3)= 2 * 3 = 6。
(2)声明为变量(tf.Variable)时,该变量会自动参与自动微分,但声明为常数(tf.constant)时,如欲参与自动微分,则需额外设定g.watch()。程序代码如下:
执行结果:与上面(1)中相同。
(3)计算二阶导数:使用tf.GradientTape()、g.gradient(y, x)函数两次,即能取得二阶导数。程序代码如下:
执行结果:一阶导数=6.0,二阶导数=2.0。
f (0)= x 2
f ′( x )= 2 x
f ″( x )= 2
f ″(3)= 2。
(4)多变量计算导数:各自使用g.gradient(y, x)函数,可取得每一个变量的梯度。若使用g.gradient()两次或以上,则tf.GradientTape()须加参数persistent=True,使tf.GradientTape()不会被自动回收,用完之后,可使用“del g”删除GradientTape对象。程序代码如下:
执行结果:d y /d x =6,d z /d x =108。
z = f ( x )= y 2 = x 4
f ′( x )= 4 x 3
f ′(3)= 108。
(5)借此机会我们认识一下PyTorch自动微分的语法,它与TensorFlow稍有差异。程序代码如下:
①requires_grad=True参数声明 x 参与自动微分。
②调用y.backward(),要求做反向传导。
③调用x.grad取得梯度。
范例. 利用TensorFlow自动微分求解简单线性回归的参数(w、b)。
程序:请参阅 03_3_简单线性回归.ipynb 。流程如图3.6所示。
图3.6 程序设计流程
(1)载入库。程序代码如下:
(2)定义损失函数 。程序代码如下:
(3)定义预测值函数 y = w x + b 。程序代码如下:
(4)定义训练函数:在自动微分中需重新计算损失函数值,assign_sub函数相当于“-=”。程序代码如下:
(5)产生随机数作为数据集,进行测试。程序代码如下:
(6)执行训练。程序代码如下:
①执行结果: w =0.9464, b =0.0326。
②损失函数值随着训练周期越来越小,如下:
(7)显示结果:回归线确实居于样本点中线。程序代码如下:
执行结果:如图3.7所示。
图3.7 自动微分求解简单线性回归执行结果
有了TensorFlow自动微分的功能,正向与反向传导变得非常简单,只要熟悉了运作的架构,后续复杂的模型就可以运用自如。
上一节运用自动微分实现了一条简单线性回归线的求解,然而神经网络是多条回归线的组合,并且每一条回归线可能再乘上非线性的Activation Function,假如使用自动微分函数逐一定义每条公式,层层串连,程序可能要很多个循环才能完成。所以为了简化程序开发的复杂度,TensorFlow/Keras直接建构了各式各样的神经层函数,可以使用神经层组合神经网络的结构,用户只需要专注算法的设计即可,轻松不少。
如图3.8所示,神经网络是多个神经层组合而成的,包括输入层(Input Layer)、隐藏层(Hidden Layer)及输出层(Output Layer),其中隐藏层可以有任意多层。一般而言,隐藏层大于或等于两层,即称为深度学习。
图3.8 神经网络示意图
TensorFlow/Keras提供了数十种神经层,分成以下类别,用户可参阅Keras官网说明(https://keras.io/api/layers/)。
(1)核心类别(Core Layer):包括完全连接层(Full Connected Layer)、激励神经层(Activation layer)、嵌入层(Embedding layer)等。
(2)卷积层(Convolutional Layer)。
(3)池化层(Pooling Layer)。
(4)循环层(Recurrent Layer)。
(5)前置处理层(Preprocessing layer):提供One-Hot Encoding、影像前置处理、数据增补(Data Augmentation)等。
我们先来看看两个最简单的完全连接层范例。
范例1. 使用完全连接层估算简单线性回归的参数( w 、 b )。
程序:请参阅03_4_简单的完全连接层.ipynb。
(1)产生随机数据,与上一节范例相同。程序代码如下:
(2)建立模型:神经网络仅使用一个完全连接层,而且输入只有一个神经元,即 X ,输出也只有一个神经元,即 y 。Dense本身有一个参数use_bias,即是否有偏差项,默认值为True,除了一个神经元输出外,还会有一个偏差项。以上设定其实就等于 y = wx + b 。为聚焦概念的说明,暂时不解释其他参数,在后面章节会有详尽说明。程序代码如下:
(3)定义模型的损失函数及优化器。程序代码如下:
(4)模型训练:只需一个指令model.fit(X, y)即可,训练过程的损失函数变化都会存在history变量中。程序代码如下:
(5)训练过程绘图。程序代码如下:
执行结果:损失函数值随着训练周期越来越小,如图3.9所示。
图3.9 训练过程执行结果
(6)取得模型参数 w 为第一层的第一个参数, b 为输出层的第一个参数。程序代码如下:
执行结果:w:0.8798,b:3.5052,因输入数据为随机随机数。
(7)绘图显示回归线。程序代码如下:
执行结果:如图3.10所示。
图3.10 绘图显示回归线执行结果
与自动微分比较,这种方法程序更简单,只要设定模型结构、损失函数、优化器后,呼叫训练(fit)函数即可。
下面我们再看一个有趣的例子,利用神经网络自动求出华氏与摄氏温度的换算公式。
范例2. 使用完全连接层推算华氏与摄氏温度的换算公式。
华氏(F)=摄氏(C)*(9/5)+ 32
(1)利用换算公式,随机产生151个数据。程序代码如下:
(2)建立模型:神经网络只有一个完全连接层,而且输入只有一个神经元,即摄氏温度,输出只有一个神经元,即华氏温度。程序代码如下:
(3)模型训练。程序代码如下:
(4)训练过程绘图。程序代码如下:
执行结果:如图3.11所示。由此可见:损失函数值随着训练周期越来越小。
图3.11 训练过程执行结果
(5)测试:输入摄氏100度及0度转换为华氏温度,答案完全正确。程序代码如下:
执行结果:
华氏(F):212.00,摄氏(C):100
华氏(F):32.00,摄氏(C):0
(6)取得模型参数 w 、 b 。
①执行结果:w:1.8000,b:31.9999,近似于华氏与摄氏温度的换算公式。
②其实换算公式也是一条回归线。
读到这里,读者应该会好奇如何使用更多的神经元和神经层,甚至更复杂的神经网络结构,下一章我们将正式迈入深度学习的殿堂,学习如何用TensorFlow解决各种实际的案例,并且详细剖析各个函数的用法及参数说明。