
2.2 Hello TensorFlow & Keras
神经网络专家Rachel Thomas曾经说过,“接触了TensorFlow后,我感觉我还是不够聪明,但有了Keras之后,事情会变得简单一些。”
他所提到的Keras是一个高级别的Python神经网络框架,能在TensorFlow上运行的一种高级的API框架。Keras拥有丰富的对数据封装和一些先进模型的实现,避免了“重复造轮子”,如图2.4所示。换言之,Keras对于提升开发者的开发效率来讲意义重大。

图2.4 TensorFlow+Keras
“不要重复造轮子。”这是TensorFlow引入Keras API的最终目的,本书还是以TensorFlow代码编写为主,Keras作为辅助工具而使用的,目的是为了简化程序编写,这点请读者一定注意。
本章非常重要,强烈建议读者独立完成每个完整代码和代码段的编写。
2.2.1 MODEL!MODEL!MODEL!还是MODEL
神经网络的核心就是模型。
任何一个神经网络的主要设计思想和功能都集中在其模型中。
TensorFlow也是如此。
TensorFlow或者其使用的高级API-Keras核心数据结构是MODEL,一种组织网络层的方式。最简单的模型是Sequential顺序模型,它由多个网络层线性堆叠。对于更复杂的结构,应该使用Keras函数式API(本书的重点就是函数式API编写),其允许构建任意的神经网络图。
为了便于理解和易于上手,作者首先从顺序Sequential开始。一个标准的顺序Sequential模型如下:

可以看到,这里首先使用创建了一个Sequential模型,之后根据需要逐级向其中添加不同的全连接层,全连接层的作用是进行矩阵计算,而相互之间又通过不同的激活函数进行激活计算(这种没有输入输出值的编程方式对有经验的程序设计人员来说并不友好,仅供举例)。
对于损失函数的计算,根据不同拟合方式和数据集的特点,需要建立不同的损失函数去最大程度地反馈拟合曲线错误。这里的损失函数采用交叉熵函数(softmax_crossentroy),使得数据计算分布能够最大限度地拟合目标值。如果对此陌生的话,读者只需要记住这些名词和下面的代码编写即可继续往下学习。代码如下:

首先通过模型计算出对应的值。这里内部采用的前向调用函数,读者知道即可。之后tf.reduce_mean计算出损失函数。
模型建立完毕后,就是数据的准备。一份简单而标准的数据,一个简单而具有指导思想的例子往往事半功倍。深度学习中最常用的一个入门起手例子Iris分类,下面就从这个例子开始,最终使用TensorFlow 2.0的Keras模式实现一个Iris鸢尾花分类的例子。
2.2.2 使用Keras API实现鸢尾花分类的例子(顺序模式)
Iris数据集是常用的分类实验数据集,由Fisher于1936年收集整理。Iris也称鸢尾花卉数据集,是一类多重变量分析的数据集。数据集包含150个数据集,分为3类,每类50个数据,每个数据包含4个属性。可通过花萼长度、花萼宽度、花瓣长度、花瓣宽度4个属性预测鸢尾花卉属于Setosa、Versicolour、Virginica这3个种类中的哪一类,如图2.5所示。

图2.5 鸢尾花
1. 第一步:数据的准备
不需要读者下载这个数据集,一般常用的机器学习工具自带Iris数据集,引入数据集的代码如下:

这里调用的是sklearn的数据库中Iris数据集,直接载入即可。
而其中的数据又是以key-value值对应存放,key值如下:

由于本例中需要Iris的特征与分类目标,因此这里只需要获取data和target。代码如下:

数据打印结果如图2.6所示。

图2.6 数据打印结果
这里是分别打印了前5条数据。可以看到Iris数据集中的特征,是分成了4个不同特征进行数据记录,而每条特征又对应于一个分类表示。
2. 第二步:数据的处理
下面就是数据处理部分,对特征的表示不需要变动。而对于分类表示的结果,全部打印结果如图2.7所示。

图2.7 数据处理
这里按数字分成了3类,0、1和2分别代表3种类型。如果按直接计算的思路可以将数据结果向固定的数字进行拟合,这是一个回归问题。即通过回归曲线去拟合出最终结果。但是本例实际上是一个分类任务,因此需要对其进行分类处理。
分类处理的一个非常简单的方法就是进行one-hot处理,即将一个序列化数据分成到不同的数据领域空间进行表示,如图2.8所示。

图2.8 one-hot处理
具体在程序处理上,读者可以手动实现one-hot的代码表示,也可以使用Keras自带的分散工具对数据进行处理,代码如下:

这里的num_classes是分成了3类,由一行三列对每个类别进行表示。
交叉熵函数与分散化表示的方法超出了本书的讲解范围,这里就不再做过多介绍,读者只需要知道交叉熵函数需要和softmax配合,从分布上向离散空间靠拢即可。

当生成的数据读取到内存中并准备以批量的形式打印,使用的是tf.data.Dataset.from_tensor_slices函数,并且可以根据具体情况对batch进行设置。tf.data.Dataset函数更多的细节和用法在后面章节中会专门介绍。
3. 第三步:梯度更新函数的写法
梯度更新函数是根据误差的幅度对数据进行更新的方法,代码如下:

与前面线性回归例子的差别是,使用的model直接获取参数的方式对数据进行不断更新而非人为指定,这点请读者注意。至于人为的指定和排除某些参数的方法属于高级程序设计,在后面的章节会介绍。
【程序2-7】

最终打印结果如图2.9所示。可以看到损失值在符合要求的条件下不停降低,达到预期目标。

图2.9 打印结果
2.2.3 使用Keras函数式编程实现鸢尾花分类的例子(重点)
我们在前面也说了,对于有编程经验的程序设计人员来说,顺序编程过于抽象,同时缺乏过多的自由度,因此在较为高级的程序设计中达不到程序设计的目标。
Keras函数式编程是定义复杂模型(如多输出模型、有向无环图,或具有共享层的模型)的方法。
让我们从一个简单的例子开始,程序2-7建立模型的方法是使用顺序编程,即通过逐级添加的方式将数据“add”到模型中。这种方式在较低级水平的编程上可以较好地减轻编程的难度,但是在自由度方面会有非常大的影响,例如当需要对输入的数据进行重新计算时,顺序编程方法就不合适。
函数式编程方法类似于传统的编程,只需要建立模型导入输出和输出“形式参数”即可。有TensorFlow 1.X编程基础的读者可以将其看作是一种新格式的“占位符”。代码如下:

下面开始逐对其进行分析。
1. 输入端
首先是input的形参:

这一点需要从源码上来看,代码如下:

input函数用于实例化Keras张量,Keras张量是来自底层后端输入的张量对象,其中增加了某些属性,使其能够通过了解模型的输入和输出来构建Keras模型。
input函数的参数:
●shape:形状元组(整数),不包括批量大小。例如shape=(32,)表示预期的输入将是32维向量的批次。
●batch_size:可选的静态批量大小(整数)。
●name:图层的可选名称字符串。在模型中应该是唯一的(不要重复使用相同的名称两次)。如果未提供,它将自动生成。
●dtype:数据类型由输入预期的,作为字符串(float32、float64、int32)。
●sparse:一个布尔值,指定是否创建占位符是稀疏的。
●tensor:可选的现有张量包裹到Input图层中。如果设置,图层将不会创建占位符张量。
●**kwargs:其他的一些参数。
上面是官方对其参数所做的解释,可以看到,这里的input函数就是根据设定的维度大小生成一个可供存放对象的张量空间,维度就是shape中设定的维度。需要注意的是,与传统的TensorFlow不同,这里的batch大小是不包含在创建的shape中。
举例来说,在一个后续的学习中会遇到MNIST数据集,即一个手写图片分类的数据集,每张图片的大小用4维来表示[1,28,28,1]。第1个数字是每个批次的大小,第2和3个数字是图片的尺寸大小,第4个1是图片通道的个数。因此输入到input中的数据为:

2. 中间层
下面每个层的写法与使用顺序模式也是不同:

在这里每个类被直接定义,之后将值作为类实例化以后的输入值进行输入计算。

因此可以看到这里与顺序最大的区别就在于实例化类以后有对应的输入端,这一点较为符合一般程序的编写习惯。
3. 输出端
对于输出端不需要额外的表示,直接将计算的最后一个层作为输出端即可:

4. 模型的组合方式
对于模型的组合方式也是很简单的,直接将输入端和输出端在模型类中显式的注明,Keras即可在后台将各个层级通过输入和输出对应的关系连接在一起。

完整的代码如下:
【程序2-8】

程序2-8的基本架构对照前面的例子没有多少变化,损失函数和梯度更新方法是固定的写法,这里最大的不同点在于,代码使用了model自带的saver函数对数据进行保存。在TensorFlow 2.0中,数据的保存是由Keras完成,即将图和对应的参数完整地保存在h5格式中。
2.2.4 使用保存的Keras模式对模型进行复用
前面已经说过,对于保存的文件,Keras是将所有的信息都保存在h5文件中,这里包含的所有模型结构信息和训练过的参数信息。

tf.keras.models.load_model函数是从给定的地址中载入h5模型,载入完成后,会依据存档自动建立一个新的模型。
模型的复用可直接调用模型predict函数:

这里是直接将Iris数据作为预测数据进行输入。全部代码如下:
【程序2-9】

最终结果如图2.10所示,可以看到计算结果被完整打印出来。

图2.10 打印结果
2.2.5 使用TensorFlow 2.0标准化编译对Iris模型进行拟合
在2.1.3小节中,作者使用了符合传统TensorFlow习惯的梯度更新方式对参数进行更新。然实际这种看起来符合编程习惯的梯度计算和更新方法,可能并不符合大多数有机器学习使用经验的读者使用。本节就以修改后的Iris分类为例,讲解标准化TensorFlow 2.0的编译方法。
对于大多数机器学习的程序设计人员来说,往往习惯了使用fit函数和compile函数对数据进行数据载入和参数分析。代码如下(请读者先运行,后面会有详细的运行分析):
【程序2-10】

下面我们详细分析一下代码。
1. 数据的获取
本例还是使用了sklearn中的Iris数据集作为数据来源,之后将target转化成one-hot的形式进行存储。顺便提一句,TensorFlow本身也带有one-hot函数,即tf.one_hot,有兴趣的读者可以自行学习。
数据读取之后的处理在后文讲解,这个问题先放一下,请读者继续往下阅读。
2. 模型的建立和参数更新
这里不准备采用新模型的建立方法,对于读者来说,熟悉函数化编程已经能够应付绝对多数的深度学习模型的建立。在后面章节中,我们会教会读者自定义某些层的方法。
对于梯度的更新,到目前为止的程序设计中都是采用了类似回传调用等方式对参数进行更新,这是由程序设计者手动完成的。然而TensorFlow 2.0推荐使用自带的梯度更新方法,代码如下:

complie函数是模型适配损失函数和选择优化器的专用函数,而fit函数的作用是把训练参数加载进模型中。下面分别对其进行讲解。
(1)compile
compile函数的作用是TensorFlow 2.0中用于配置训练模型专用编译函数。源码如下:

这里我们主要介绍其中最重要的3个参数optimizer、loss和metrics。
●optimizer:字符串(优化器名)或者优化器实例。
●loss:字符串(目标函数名)或目标函数。如果模型具有多个输出,可以通过传递损失函数的字典或列表,在每个输出上使用不同的损失。模型最小化的损失值将是所有单个损失的总和。
●metrics:在训练和测试期间的模型评估标准。通常会使用metrics=['accuracy']。要为多输出模型的不同输出指定不同的评估标准,还可以传递一个字典,如metrics={'output_a':'accuracy'}。
●可以看到,优化器(optimizer)被传入了选定的优化器函数,loss是损失函数,这里也被传入选定的多分类crossentry函数。metrics用来评估模型的标准,一般用准确率表示。
实际上,compile编译函数是一个多重回调函数的集合,对于所有的参数来说,实际上就是根据对应函数的“地址”回调对应的函数,并将参数传入。
举个例子,在上面编译器中我们传递的是一个TensorFlow 2.0自带的损失函数,而实际上往往由于针对不同的计算和误差需要不同的损失函数,这里自定义一个均方差(MSE)损失函数,代码如下:

这个损失函数接收2个参数,分别是y_true和y_pred,即预测值和真实值的形式参数。之后根据需要计算出真实值和预测值之间的误差。
损失函数名作为地址传递给compile后,即可作为自定义的损失函数在模型中进行编译。代码如下:

至于优化器的自定义实际上也是可以的。但是,一般情况下优化器的编写需要比较高的编程技巧以及对模型的理解,这里读者直接使用TensorFlow 2.0自带的优化器即可。
(2)fit
fit函数的作用是以给定数量的轮次(数据集上的迭代)训练模型。其主要参数有如下4个:
●x:训练数据的NumPy数组(如果模型只有一个输入),或者是NumPy数组的列表(如果模型有多个输入)。如果模型中的输入层被命名,你也可以传递一个字典,将输入层名称映射到NumPy数组。如果从本地框架张量馈送(例如TensorFlow数据张量)数据,x可以是None(默认)。
●y:目标(标签)数据的NumPy数组(如果模型只有一个输出),或者是NumPy数组的列表(如果模型有多个输出)。如果模型中的输出层被命名,你也可以传递一个字典,将输出层名称映射到NumPy数组。如果从本地框架张量馈送(例如TensorFlow数据张量)数据,y可以是None(默认)。
●batch_size:整数或None。每次梯度更新的样本数。如果未指定,默认为32。
●epochs:整数。训练模型迭代轮次。一个轮次是在整个x和y上的一轮迭代。请注意,与initial_epoch一起,epochs被理解为“最终轮次”。模型并不是训练了epochs轮,而是到第epochs轮停止训练。
fit函数的主要作用就是对输入的数据进行修改,如果读者已经成功运行了程序2-10,那么现在换一种略微修改后的代码,重新运行Iris数据集。代码如下:
【程序2-11】

对比程序2-10和程序2-11可以看到,它们最大的不同在于数据读取方式的变化。更为细节的做出比较,在程序2-10中,数据的读取方式和fit函数的载入方式如下:

Iris的数据读取被分成2个部分,分别是数据特征部分和label分布。而label部分使用Keras自带的工具进行离散化处理。
离散化后处理的部分又被tf.data.Dataset API整合成一个新的数据集,并且依batch被切分成多个部分。
此时fit的处理对象是一个被tf.data.Dataset API处理后的一个Tensor类型数据,并且在切分的时候依照整合的内容被依次读取。在读取的过程中,由于它是一个Tensor类型的数据,fit内部的batch_size划分不起作用,而使用生成数据的tf中数据生成器的batch_size划分。如果读者对其还是不能够理解的话,可以使用如下代码段打印重新整合后的train_data中的数据看看,代码如下:

现在回到程序2-11中,作者取出对应于数据读取和载入的部分如下:

可以看到数据在读取和载入的过程中没有变化,将处理后的数据直接输入到fit函数中供模式使用。此时由于是直接对数据进行操作,因此对数据的划分由fit函数负责,此时fit函数中的batch_size被设定为128。
2.2.6 多输入单一输出TensorFlow 2.0编译方法(选学)
在前面内容的学习中,我们采用的是标准化的深度学习流程,即数据的准备与处理、数据的输入与计算,以及最后结果的打印。虽然在真实情况中可能会遇到各种各样的问题,但是基本步骤是不会变的。
这里存在一个非常重要的问题,在模型的计算过程中,如果遇到多个数据输入端应该怎么处理,如图2.11所示。

图2.11 多个数据输入端
以Tensor格式的数据为例,在数据的转化部分就需要将数据进行“打包”处理,即将不同的数据按类型进行打包。如下所示:

请注意小括号的位置,这里显示的将数据分成2个部分,输入与标签两类。而多输入的部分被使用小括号打包在一起形成一个整体。
下面还是以Iris数据集为例讲解多数据输入的问题。
1. 第一步:数据的获取与处理
从前面的介绍可以知道,Iris数据集每行是一个由4个特征组合在一起表示的特征集合,此时可以人为地将其切分,即将长度为4的特征转化成一个长度为3和一个长度为1的两个特征集合。代码如下:

打印其中的一条数据,如下所示:

可以看到,一行4列的数据被拆分成2组特征。
2. 第二步:模型的建立
接下来就是模型的建立,这里数据被人为地拆分成2个部分,因此在模型的输入端,也要能够对应处理2组数据的输入。

可以看到代码中分别建立了input_xs_1和input_xs_2作为数据的接收端接受传递进来的数据,之后通过一个concat重新将数据组合起来,恢复成一条4特征的集合。

对剩余部分的数据处理没有变化,按前文程序处理即可。
3. 第三步:数据的组合
切分后的数据需要重新对其进行组合,生成能够符合模型需求的Tensor数据。这里最为关键的是在模型中对输入输出格式的定义,把模式的输入输出格式拆出如下:

因此在Tensor建立的过程中,也要按模型输入的格式创建对应的数据集。格式如下:

请注意这里的括号有几重,这里我们采用了2层括号对数据进行包裹,即首先将输入1和输入2包裹成一个输入数据,之后重新打包输出,共同组成一个数据集。转化Tensor数据代码如下:

注意
请读者一定要注意小括号的层数。
完整代码如下:
【程序2-12】

最终结算结果如图2.12所示。

图2.12 打印结果
对于认真阅读本书的读者来说,这个最终的打印结果应该见过很多次了,在这里TensorFlow 2.0默认输出了每个循环结束后的loss值,并且按compile函数中设定的内容输出准确率(accuarcy)值。最后的evaluate函数是通过对测试集中的数据进行重新计算,从而获取在测试集中的损失值和准确率。本例使用训练数据代替测试数据。
在程序2-12中数据的准备是使用tf.data API完成,即通过打包的方式将数据输出,也可以直接将输入的数据输入到模型中进行训练。代码如下:
【程序2-13】

最终打印结果请读者自行验证,需要注意的是其中数据的包裹情况。
2.2.7 多输入多输出TensorFlow 2.0编译方法(选学)
读者已经知道了多输入单一输出的TensorFlow 2.0的写法,而在实际编程中有没有可能遇到多输入多输出的情况。
事实上是有的。虽然读者可能遇到的情况会很少,但是在必要的时候还是需要设计多输出的神经网络模型去进行训练,例如“bert”模型。
对于多输出模型的写法,实际上也可以仿照单一输出模型改为多输入模型的写法,将output的数据使用中括号进行包裹。

首先是对数据的修正和设计,数据的输入被平均分成2组,每组有2个特征。这实际上没什么变化。而对于特征的分类,在引入one-hot处理的分类数据集外,还保留了数据分类本身的真实值作目标的辅助分类计算结果。而无论是多输入还是多输出,此时都使用打包的形式将数据重新打包成一个整体的数据集合。
在fit函数中,直接是调用了打包后的输入数据即可。

完整代码如下:
【程序2-14】

输出结果如图2.13所示。

图2.13 打印结果
限于篇幅关系,这里也只给出一部分结果,相信读者能够理解输出的数据内容。