参考链接: Tensorflow 2.0的新功能
Tensorflow 2.0+与Keras的联系与应用(含model详解)
事实上我个人入坑tensorflow比较晚,于是导致我其实并没有经历Tensorflow_v1特别火热的那个年代,今年(2020年)早些时候,Tensorflow_v2已经成熟并且开始大量的出现在技术干货当中,于是,我相当于跳过了那个需要写sess的过程,直接学习了函数式model定义以及Keras,可以说是非常幸福了(因为确实简化了很多工作)但遗憾的是,在机器学习和深度学习过程中,大量的前辈的文章,赖以实现的源代码和demo使用的依然是v1的代码,所以如果有余力还是很有必要去了解一些v1的代码写法,至少要能看得懂才可以。
其实在一开始学习的时候,我就在试图挖掘和总结v1与v2的区别,但是无奈细节太多,倒不如从v2有什么开始介绍会更好理解,那么最突出的一点应该就是v2与Keras更好的结合吧,避免了很多我们重复造轮子的过程。本文主要为了比较全面的介绍Tensorflow_v2与Keras的关系,同时全文伴随着举出一个比较经典的mnist卷积案例来展示一下Keras的魅力。
Keras API加入Tensorflow
Keras事实上是一个高级别的Python神经网络框架,能够在Tensorflow上运行的一款高级的API框架,它拥有着丰富的数据封装和一些先进的模型实现,避免了“重复造轮子”。并且Keras.datasets库提供了一些经典的机器学习数据集的下载API,比如Mnist和IMDB数据集可以直接通过API下载专有的格式,而且为了方便初学者的使用,Keras对数据集已经进行过很好的清洗,大家可以放心的用合适的方法和函数去提取数据(网上可以搜到细节教程,这不是我们的重点,这里不赘述)然后去直接测试自己的模型,而不用花大量的时间去清洗数据。当然最主要的还是Keras定义好了很多我们的常用操作,避免了重复造轮子的尴尬,对于提升开发者效率来说意义重大,同时也是Tensorflow引入Keras API的最主要目的。
事实上我们还是以Tensorflow代码为主,Keras只是我们的一个方便的辅助工具,它会简化我们的程序编写过程。
Pic_1: Tensorflow引入Keras API
Keras Model(非常关键)
神经网络的核心就是model。任何一个神经网络的主要设计思想和功能都集中在model中。Keras的加入使得model的定义更加简单了。其中,最简单的就是序列模型Sequential model,它由多个网络层堆叠而成,顺序执行,一层一层逻辑关系非常清晰,易于构建和理解。但是现在我们在解决实际应用场景中的问题的时候会发现,很多问题并不能简单地解决,可能大部分时间我们现在要想在现有基础上进一步改进,都需要制作更庞大复杂的模型。这时候就应该使用Keras的函数式格式来定义functional model(这也是我们接下来的重点,因为它真的很重要),它可以支持我们构建任意结构的神经网络图。
Sequential model序列模型
但是我们一开始还是从介绍简单的Sequential model开始,因为有比较才能觉察出functional model的优势在哪儿。比如:
from tensorflow import keras as Keras
# Keras的Sequential model序列模型举例
model = Keras.models.Sequential() # 创建一个Keras的Sequential模型
model.add(Keras.layers.Dense(128, activation='relu')) # 增加一个128个隐藏神经元的全连接层
model.add(Keras.layers.Dense(64, activation='relu')) # 增加一个64个隐藏神经元的全连接层
model.add(Keras.layers.Dense(16, activation='relu')) # 增加一个16个隐藏神经元的全连接层
model.add(Keras.layers.Dense(1, activation='softmax')) # 增加一个softmax归一化输出层
可以看到,首先创建了一个Sequential模型,然后根据我们实际的需要,在model里面堆叠我们想要的神经网络层就可以了,在这里仅仅是拿了4个简单的全连接层来做示范,对于卷积神经网络而言就将是很多conv层和pooling层。
Functional model函数式模型(重点)
顺序模型对于问题解释程度较差缺乏自由度,所以如果想要实现更为复杂的模型仅仅使用Sequential model就显得不太够,如果想要定义复杂模型(比如多输出模型、有向无环图或者具有共享层的模型)就应该使用Keras提供的函数式model定义法。
第一次接触这种写法会感觉很奇怪,但是习惯之后就发现其实还是很好理解的,只不过传参的过程和调用的目标现在具有更好的自由度,它在形式上非常类似于传统的编程,制需要建立模型导入输出和输出“形式参数”即可。如果之前学过tensorflow_v1可以近似将其理解为一种新格式的“占位符”(其实是为输入提前申请了一个张量空间),在这里也给出一个简单的小例子代码:(注释是我之前做案例的时候加的,懒得删掉了,对于理解也有帮助)
# 使用Input类进行初始化输入,根据输入数据的大小将输入的数据维度做成[28,28,1]
input_data = tf.keras.Input([28, 28, 1]) # 与之前v1不同的是batch_size不需要设置了,tensorflow2.3自己能识别
# 首先是一个32个3*3核的卷积层,补零,激活函数也不用自己再写了,直接封装在里面了使用relu,并且用input_data初始化了整个卷积类
conv = tf.keras.layers.Conv2D(filters=32, kernel_size=3, padding="SAME", activation=tf.nn.relu)(input_data)
# 然后是使用BatchNormalization正则化类作为正则化工具也被用作各个层之间的联结(减小模型过拟合可能,并增强模型泛化能力)
conv = tf.keras.layers.BatchNormalization()(conv)
# 然后接一个64个3*3核的卷积层,补零,激活函数为relu(特别备注一下tensorflow2.0对于kernel_size=3有自己封装好的优化效果,尽量多用3)
conv = tf.keras.layers.Conv2D(filters=64, kernel_size=3, padding="SAME", activation=tf.nn.relu)(conv)
# 然后进行一次最大值池化(也是为了降低过拟合,增加模型泛化能力)
conv = tf.keras.layers.MaxPool2D(strides=[1, 1])(conv)
# 然后再接一个128*3*3的卷积层,补零,激活函数为relu
conv = tf.keras.layers.Conv2D(128, 3, padding="SAME", activation=tf.nn.relu)(conv)
# 然后接一个Flatten层将数据压扁(平整化)成全连接神经网络能使用格式
flat = tf.keras.layers.Flatten()(conv)
# 然后接一个全连接层隐藏层设置128个神经元,激活函数使用relu(全连接层的目标是对卷积后的结果进行最终分类)
dense = tf.keras.layers.Dense(128, activation=tf.nn.relu)(flat)
# 将特征提取为10个输出维度进行最终分类使用softmax激活函数进行特征归一化
output_data = tf.keras.layers.Dense(10, activation=tf.nn.softmax)(dense)
# 定义好卷积神经网络的起止结点(即执行刚才定义好的模型从input_data开始到output_data结束,可以理解为规定模型定义部分的上下界)
model = tf.keras.Model(inputs=input_data, outputs=output_data)
关于这一部分,我们仔细的拿出来讲,可以看到我上面给出的例子,这是笔者之前自己做的一个卷积的函数式模型,它包含很多与我们之前说的Sequential model不同的地方,出现了很多我们之前不了解的参数和方法,现在我们选择其中必要的部分进行讲解。
第零层:输入层
首先第一部分是输入端,也就是我在代码中第一行是用的Input初始化方法,可以发现其实Input是Keras类当中的一个layers层,或者可以将其看做输入层的概念。Input函数其实适用于实例化Keras张量,Keras张量是来自底层后端输入的张量对象(就是我们处理好的数据集当中一个对象的尺寸),其中当然又增加了一些属性,使其能够通过了解模型的输入输出来构建Keras完整的model模型。属性如下:
@keras_export('keras.Input', 'keras.layers.Input')
def Input( # pylint: disable=invalid-name
shape=None,
batch_size=None,
name=None,
dtype=None,
sparse=False,
tensor=None,
ragged=False,
**kwargs):
"""`Input()` is used to instantiate a Keras tensor.
根据类库的定义我们可以一个一个的来解读其参数:
shape(必要参数):可以理解成输入张量的形状(也就是我们的模型数据输入每一个batch的尺寸了),是必须传的参数(要求必须是整数值),官方wiki称其为形状元组(但是我觉得这样称呼不好理解),举个例子吧,例如输入shape = (32,)表示预期输入将是32维向量的批次。batch_size:可选的批量大小(也必须是整数值)name:输入层的可选名称字符串。在模型中应该是唯一的(不同的层不要重复使用相同的名称,会导致后期如果想用名称调用某一层的时候难以辨识),其实和给自定义张量起名一样,这个值可以不赋,tensorflow在编译时会自动生成。dtype:输入层数据类型的预期格式(常见输入类型包括float32, float64, int32)sparse:一个布尔值,表示是否创建的输入空间(占位符)是稀疏的。(一般默认就好,不用我们赋值)tensor:将可选的现有输入张量加载到Input层中。如果设置了的话,输入层将不会创建空白输入空间占位符张量。**kwargs:其他的一些参数,开源部分,方便tensorflow开发者扩展该方法。
可以发现Input函数事实上是创建了一个输入空间,这个输入空间是一个可供存放对象的张量空间,维度的shape就是输入的维度,需要注意的是,它与传统的Tensorflow不同,这里的batchsize是通过batch_size单独一个参数进行设置的,不包含在shape参数中。需要注意。
但是这样其实还是不太直观,要想更好地理解shape可以看看我上面的代码,这个输入的其实是mnist数据集,即手写分类识别数据集,每张图片的大小需要用4维来表示[1, 28, 28, 1]。第一个数字是批次的大小(每次一张图片),第二、三个数字是图片尺寸为28*28,第四个数字是通道个数(图片是灰度图片所以只有1个通道,RGB的话应该是3个通道)。
当然你会发现我写的是[28,28,1]并没有四维啊?这是因为tensorflow_v2.3非常厉害可以自动发现你现在在做的是一个图像识别的任务,所以你既不用在shape里写上batch,也不用单独定义batch_size的前提下keras也知道你希望如何去处理这个数据集。
# 使用Input类进行初始化输入,根据输入数据的大小将输入的数据维度做成[28,28,1]
input_data = tf.keras.Input([28, 28, 1]) # 与之前v1不同的是batch_size不需要设置了,tensorflow2.3自己能识别
第1 ~ n-1层:中间层
刚才这上面讲的都是关于输入层的设计,它定义了整个model的输入形式以及batch_size等信息。那么主要进行操作的其实还是中间层的设计,中间层的定义也与之前Sequential model有很大的不同。我们直接看例子中的这一部分:
# 首先是一个32个3*3核的卷积层,补零,激活函数也不用自己再写了,直接封装在里面了使用relu,并且用input_data初始化了整个卷积类
conv = tf.keras.layers.Conv2D(filters=32, kernel_size=3, padding="SAME", activation=tf.nn.relu)(input_data)
# 然后是使用BatchNormalization正则化类作为正则化工具也被用作各个层之间的联结(减小模型过拟合可能,并增强模型泛化能力)
conv = tf.keras.layers.BatchNormalization()(conv)
# 然后接一个64个3*3核的卷积层,补零,激活函数为relu(特别备注一下tensorflow2.0对于kernel_size=3有自己封装好的优化效果,尽量多用3)
conv = tf.keras.layers.Conv2D(filters=64, kernel_size=3, padding="SAME", activation=tf.nn.relu)(conv)
# 然后进行一次最大值池化(也是为了降低过拟合,增加模型泛化能力)
conv = tf.keras.layers.MaxPool2D(strides=[1, 1])(conv)
# 然后再接一个128*3*3的卷积层,补零,激活函数为relu
conv = tf.keras.layers.Conv2D(128, 3, padding="SAME", activation=tf.nn.relu)(conv)
# 然后接一个Flatten层将数据压扁(平整化)成全连接神经网络能使用格式
flat = tf.keras.layers.Flatten()(conv)
# 然后接一个全连接层隐藏层设置128个神经元,激活函数使用relu(全连接层的目标是对卷积后的结果进行最终分类)
dense = tf.keras.layers.Dense(128, activation=tf.nn.relu)(flat)
可以看到在这里每个类被直接定义,之后将值作为类实例化以后的输入值进行输入计算。写法上也是有着很大的不同。最直接的表现为,我们不同去定义每一层的输入参数了,而是在每个类后面再写一个括号,里面放上输入信息,这直接就导致编程的灵活度提高了很多,因为输入不是直接的层级关系,而可以进行跨越和反复调用,这完全取决于变量名称形式的调用关系,而不再是Sequential的流程主导控制。一开始肯定会不太习惯,但越来越觉得这种写法很合理而且很好理解。(其实如果你想让它顺序执行很简单,就可以按照我例子中的写法,每一层的输入其实是上一层的输出,但这样也很方便,因为我们不用起一大堆变量名了,而一直使用conv就可以)
第n层:输出层
其实输出层很简单,一般我们的模型最后都会有一个归一化输出层,这个层一般是个神经元特别少的全连接层,用于做分类预测输出(激活函数一般是softmax)。
# 将特征提取为10个输出维度进行最终分类使用softmax激活函数进行特征归一化
output_data = tf.keras.layers.Dense(10, activation=tf.nn.softmax)(dense)
第n+1层:整合层
要记得我们定义完每一层之后,还要对我们的模型定义一个起点和终点,其实就相当于对我们刚才的构建过程打一个包,告诉keras我们从哪里开始,从哪里结束。方法写法如下:
# 定义好卷积神经网络的起止结点(即执行刚才定义好的模型从input_data开始到output_data结束,可以理解为规定模型定义部分的上下界)
model = tf.keras.Model(inputs=input_data, outputs=output_data)
就是将模型的起始层和输出层告诉keras.model。
模型的应用model.compile()
# compile函数是tensorflow_v2适配损失函数和选择优化器的专用函数
# (使用Adam优化函数来优化梯度和学习率,损失函数为交叉熵损失函数,metrics使模型的评价标准,一般默认就是精准匹配模式)
model.compile(optimizer=tf.optimizers.Adam(1e-3), loss=tf.losses.categorical_crossentropy, metrics=['accuracy'])
无论我们是使用sequential model还是functional model定义方法,最后都要进行Model.compile来将模型的损失函数和优化方法进行定义。比如我上面的代码中使用的就是Adam优化方法,和交叉熵损失函数,后面是用的是精准metrics评价函数。关于metrics评价函数这个大家不熟悉的参数以及它与损失函数的对应关系,大家可以参考我的另一篇文章来进行学习https://blog.csdn.net/qq_39381654/article/details/108747701,和本文一样都是个人总结以便自查,大家也可以去看看会有所帮助。跑题了……
总之,模型定义好了就该去定义这各compile方法了,通过改变参数去选择你的模型的优化方法,损失函数和评价函数。
让tensorflow训练刚刚定义好的模型
当我们定义好了模型之后,肯定是要开始加载训练集去训练模型咯,方法和以前一样,使用的是model.fit方法,可以在这里设置epoch整体迭代次数。
model.fit(train_dataset, epochs=3)
事实上这个过程也可以设定打印的时候的显示模式可以在后面加上verbose这个参数,verbose = 0不打印过程进度条,只打印每一个epoch的结果,verbose = 1会打印epoch训练进度条(我比较喜欢进度条),而如果你设置了verbose = 2就什么都不打印,训练完就完了(非常不推荐,这样的话看不到每次epoch的acc和loss变化没法得知epoch或者参数设置是否导致模型欠拟合或者过拟合)比如下面我给出的这是我做的另外一个RNN实验的model.fit写法,是不是比刚才复杂多了。
model.fit(trainSet, labelTrain, batch_size=400, epochs=7, verbose=1, validation_split=0.2)
首先,这是一个自带标注的数据集,标注集被单独分离出来了所以传参的时候传两个,另外,这个数据集的数量比较庞大,需要自行设置epoch的batch_size,而且你会发现我最后还以这个数据集的20%抽取出来作为每一次epoch的验证集,有助于我得到每轮迭代的预测loss和训练集loss作比较,来方便调参。(这里涉及调参和epoch的打印信息含义,我想以后再单独写篇文章解释吧,一篇文章东西太多不太好)。
训练好的模型mdoel的保存与复用
# 使用Keras的model将定义好的模型进行保存用以随后的复用
model.save("model_saved.h5")
可以看到,我调用了model.save方法来保存我刚刚训练好的模型,这很有用。举个例子,很多时候我们的集群在云端帮我们泡好了一个模型的数据,但是不巧我们在服务器上接收不到我们代码里的matplotlib绘制的数据,我们就可以让服务器执行model.save把训练好的模型保存在一个.h5文件里,然后拷贝到本地,直接加载模型进行预测,并且画出预测图和你想要的数据,甚至将该模型作为预训练模型来做其他的事。
所以在这里我再介绍一下如何使用之前保存好的模型,其实很简单,一行代码的事:
new_model = tf.keras.models.load_model("model_saved.h5")
然后再拿这个new_model去执行模型预测就可以直接呈现预测结果了:
new_model = tf.keras.models.load_model("model_saved.h5")
new_prediction = new_model.predict(test_dataset)
稍作总结
刚才我们进行了一个比较完整的Model定义和使用过程,我会把这个过程中使用的源码打包放在这里供大家使用和对照,其实你会发现tensorflow_v2真的比v1省了很多事,我们不用定义好模型再去sess了,取而代之的是一套非常流畅的模型定义和使用过程,当然这样高度的封装也带来另外一个问题就是,过于简单的使用或许不适合我们去理解模型本身的具体实现,或者对模型底层实现改变或创新,但假如你做的是更高层或者深层DNN的模型的搭建,那么我个人觉得不要重复造轮子,这些模型的效率已经很高了,我们可以在其他角度上改进和创新,tensorflow_v2和keras给我们提供了一个很好的平台来帮助我们构建效率更高的模型和实现更多的现实任务,我们应该尽快熟悉并掌握它,来更高效地构建自己的模型和代码。当然我们都是站在巨人的肩膀上学习,向前辈致敬,并保持学习才是我们的态度。
附上本文的案例代码打包文件
相信大家已经看了本文的代码很久了,想要自己跑跑试试了。这里打包上传了我的mnist模型,但是再次提醒如果大家没有配置GPU的话,跑这个实验还是挺难的,建议要么用服务器,要么自己去先参考我之前发的文章配个GPU+CUDA+CUDNN来训练模型。(文章链接:https://blog.csdn.net/qq_39381654/article/details/108063967)
代码已经上传,并且通过了验证:https://download.csdn.net/download/qq_39381654/12914599(是这样,我发现下载的人多了之后它会变贵,其实我一开始仅仅标记了1积分。所以如果它变得特别贵的时候私信我,我再把它调回去,可能平台也是为了照顾作者,先谢谢平台了)