《TensorFlow 2.0深度学习算法实战教材》重点内容摘录(七、Kears高层接口)
Keras 是一个主要由Python 语言开发的开源神经网络计算库。Keras 库分为前端和后端,其中后端可以基于现有的深度学习框架实现,如Theano,CNTK,TensorFlow,前端接口即Keras抽象过的统一接口API。
那么 Keras 与tf.keras 有什么区别与联系呢?其实Keras 可以理解为一套搭建与训练神经网络的高层API 协议,Keras 本身已经实现了此协议,可以方便的调用TensorFlow,CNTK 等后端完成加速计算;在TensorFlow 中,也实现了一套Keras 协议,即tf.keras,但只能基于TensorFlow 后端计算,并对TensorFlow 的支持更好。对于使用TensorFlow 的开发者来说,tf.keras 可以理解为一个普通的子模块,与其他子模块,如tf.math,tf.data 等并没有什么差别。
常见功能模块
Keras 提供了一系列高层的神经网络类和函数,如常见数据集加载函数,网络层类,模型容器,损失函数类,优化器类,经典模型类等等。
常见网络层类
- tf.nn 模块:对于常见的神经网络层,可以使用张量方式的底层接口函数来实现,这些接口函数一般在tf.nn 模块中
- tf.keras.layers命名空间:提供了大量常见网络层的类接口,如全连接层,**含水层,池化层,卷积层,循环神经网络层等等
以 Softmax 层为例,它既可以使用tf.nn.softmax 函数在前向传播逻辑中完成Softmax 运算,也可以通过layers.Softmax(axis)类搭建Softmax 网络层,其中axis 参数指定进行softmax 运算的维度。
import tensorflow as tf
# 导入keras 模型,不能使用import keras,它导入的是标准的Keras 库
from tensorflow import keras
from tensorflow.keras import layers # 导入常见网络层类
然后创建Softmax 层,并调用__call__方法完成前向计算:
In [1]:
x = tf.constant([2.,1.,0.1])
layer = layers.Softmax(axis=-1) # 创建Softmax 层
layer(x) # 调用softmax 前向计算
经过Softmax 网络层后,得到概率分布为:
网络容器
对于常见的网络,需要手动调用每一层的类实例完成前向传播运算,当网络层数变得较深时,这一部分代码显得非常臃肿。可以通过Keras 提供的网络容器Sequential 将多个网络层封装成一个大网络模型,只需要调用网络模型的实例一次即可完成数据从第一层到最末层的顺序运算。
例如,2 层的全连接层加上单独的**函数层,可以通过Sequential 容器封装为一个网络。
# 导入Sequential 容器
from tensorflow.keras import layers, Sequential
network = Sequential([ # 封装为一个网络
layers.Dense(3, activation=None), # 全连接层
layers.ReLU(),#**函数层
layers.Dense(2, activation=None), # 全连接层
layers.ReLU() #**函数层
])
x = tf.random.normal([4,3])
network(x) # 输入从第一层开始,逐层传播至最末层
Sequential 容器也可以通过add()方法继续追加新的网络层,实现动态创建网络的功能:
In [2]:
layers_num = 2 # 堆叠2 次
network = Sequential([]) # 先创建空的网络
for _ in range(layers_num):
network.add(layers.Dense(3)) # 添加全连接层
network.add(layers.ReLU())# 添加**函数层
network.build(input_shape=(None, 4)) # 创建网络参数
network.summary()
通过summary()函数可以方便打印出网络结构和参数量:
可以看到Layer 列为每层的名字,这个名字由TensorFlow 内部维护,与Python 的对象名不一样,Param#列为层的参数个数,Total params 统计出了总的参数量,Trainable params 为待优化的参数量,Non-trainable params 为不需要优化的参数量。
当我们通过Sequential 容量封装多层网络层时,所有层的参数列表将会自动并入Sequential 容器的参数列表中,不需要人为合并网络参数列表。Sequential 对象的trainable_variables 和variables 包含了所有层的待优化张量列表和全部张量列表:
In [3]: # 打印网络的待优化参数名与shape
for p in network.trainable_variables:
print(p.name, p.shape)
Out[3]:
dense_2/kernel:0 (4, 3)
dense_2/bias:0 (3,)
dense_3/kernel:0 (3, 3)
dense_3/bias:0 (3,)
模型装配、训练与测试
在训练网络时,一般的流程是通过前向计算获得网络的输出值,再通过损失函数计算网络误差,然后通过自动求导工具计算梯度并更新,同时间隔性地测试网络的性能。
模型装配
在 Keras 中,有2 个比较特殊的类:keras.Model 和keras.layers.Layer 类。其中Layer类是网络层的母类,定义了网络层的一些常见功能,如添加权值,管理权值列表等。Model 类是网络的母类,除了具有Layer 类的功能,还添加了保存、加载模型,训练与测试模型等便捷功能。Sequential 也是Model 的子类,因此具有Model 类的所有功能。
接下来介绍Model 及其子类的模型装配与训练功能。我们以Sequential 容器封装的网络为例,首先创建5 层的全连接网络用于MNIST 手写数字图片识别:
# 创建5 层的全连接层网络
network = Sequential([layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(10)])
network.build(input_shape=(None, 28*28))
network.summary()
创建网络后,正常的流程是通过循环迭代数据集多遍,每次按批产生训练数据,前向计算,然后通过损失函数计算误差值,并反向传播自动计算梯度,更新网络参数。这一部分逻辑由于非常通用,在keras 中提供了compile()和fit()函数方便实现上述逻辑。首先通过compile 函数指定网络使用的优化器对象,损失函数,评价指标等:
# 导入优化器,损失函数模块
from tensorflow.keras import optimizers,losses
# 采用Adam 优化器,学习率为0.01;采用交叉熵损失函数,包含Softmax
network.compile(optimizer=optimizers.Adam(lr=0.01),
loss=losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'] # 设置测量指标为准确率
)
我们在compile()函数中指定的优化器,损失函数等参数也是我们自行训练时需要使用的参数,并没有什么特别之处,只不过keras 将这部分常用逻辑实现了,提高开发效率。
模型训练
模型装配完成后,即可通过fit()函数送入待训练的数据和验证用的数据集:
# 指定训练集为train_db,验证集为val_db,训练5 个epochs,每2 个epoch 验证一次
# 返回训练信息保存在history 中
history = network.fit(train_db, epochs=5, validation_data=val_db,
validation_freq=2)
其中train_db 为tf.data.Dataset 对象,也可以传入Numpy Array 类型的数据;epochs 指定训练迭代的epochs 数;validation_data 指定用于验证(测试)的数据集和验证的频率validation_freq。
运行上述代码即可实现网络的训练与验证的功能,fit 函数会返回训练过程的数据记录history,其中history.history 为字典对象,包含了训练过程中的loss,测量指标等记录项:
In [4]: history.history # 打印训练记录
Out[4]: # 训练准确率
{'accuracy': [0.00011666667, 0.0, 0.0, 0.010666667, 0.02495],
'loss': [2465719710540.5845, # 训练误差
78167808898516.03,
404488834518159.6,
1049151145155144.4,
1969370184858451.0],
'val_accuracy': [0.0, 0.0], # 验证准确率
# 验证误差
'val_loss': [197178788071657.3, 1506234836955706.2]}
可以看到通过compile&fit 方式实现的代码非常简洁和高效,大大缩减了开发时间。但是因为接口非常高层,灵活性也降低了,是否使用需要用户自行判断。
模型测试
通过 Model.predict(x)方法即可完成模型的预测:
# 加载一个batch 的测试数据
x,y = next(iter(db_test))
print('predict x:', x.shape)
out = network.predict(x) # 模型预测
print(out)
其中out 即为网络输出。
如果只是简单的测试模型的性能,可以通过Model.evaluate(db)即可循环测试完db 数据集上所有样本,并打印出性能指标:
network.evaluate(db_test) # 模型测试
模型保存与加载
模型训练完成后,需要将模型保存到文件系统上,从而方便后续的模型测试与部署工作。实际上,在训练时间隔性地保存模型状态也是非常好的习惯.
在 Keras 中,有三种常用的模型保存与加载方法。
张量方式
网络的状态主要体现在网络的结构以及网络层内部张量参数上,因此在拥有网络结构源文件的条件下,直接保存网络张量参数到文件上是最轻量级的一种方式。我们以MNIST手写数字图片识别模型为例,通过调用Model.save_weights(path)方法即可讲当前的网络参数保存到path 文件上:
network.save_weights('weights.ckpt')
上述代码将network 模型保存到weights.ckpt 文件上,在需要的时候,只需要先创建好网络对象,然后调用网络对象的load_weights(path)方法即可将指定的模型文件中保存的张量数值写入到当前网络参数中去:
# 保存模型参数到文件上
network.save_weights('weights.ckpt')
print('saved weights.')
del network # 删除网络对象
# 重新创建相同的网络结构
network = Sequential([layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(10)])
network.compile(optimizer=optimizers.Adam(lr=0.01),
loss=tf.losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy']
)
# 从参数文件中读取数据并写入当前网络
network.load_weights('weights.ckpt')
print('loaded weights!')
这种保存与加载网络的方式最为轻量级,文件中保存的仅仅是参数张量的数值,并没有其他额外的结构参数。但是它需要使用相同的网络结构才能够恢复网络状态,因此一般在拥有网络源文件的情况下使用。
网络方式
我们来介绍一种不需要网络源文件,仅仅需要模型参数文件即可恢复出网络模型的方式。通过Model.save(path)函数可以将模型的结构以及模型的参数保存到一个path 文件上,在不需要网络源文件的条件下,通过keras.models.load_model(path)即可恢复网络结构和网络参数。
我们首先将MNIST 手写数字图片识别模型保存到文件上,并且删除网络对象:
# 保存模型结构与模型参数到文件
network.save('model.h5')
print('saved total model.')
del network # 删除网络对象
此时通过model.h5 文件即可恢复出网络的结构和状态:
# 从文件恢复网络结构与网络参数
network = tf.keras.models.load_model('model.h5')
可以看到,model.h5 文件除了保存了模型参数外,还保存了网络结构信息,不需要提前创建模型即可直接从文件中恢复出网络network 对象。
SavedModel 方式
当需要将模型部署到其他平台时,采用TensorFlow 提出的SavedModel 方式更具有平台无关性。通过 tf.keras.experimental.export_saved_model(network, path)即可将模型以SavedModel方式保存到path 目录中:
# 保存模型结构与模型参数到文件
tf.keras.experimental.export_saved_model(network, 'model-savedmodel')
print('export saved model.')
del network # 删除网络对象
此时在文件系统model-savedmodel 目录上出现了如下网络文件:
用户无需关心文件的保存格式,只需要通过
# 从文件恢复网络结构与网络参数
network = tf.keras.experimental.load_from_saved_model('model-savedmodel')
即可恢复出网络结构和参数,方便各个平台能够无缝对接训练好的网络模型。
自定义类
尽管 Keras 提供了很多的常用网络层,但深度学习可以使用的网络层远远不止这些经典的网络层,对于需要创建自定义逻辑的网络层,可以通过自定义类来实现。在创建自定义网络层类时,需要继承自layers.Layer 基类;创建自定义的网络类,需要继承自keras.Model 基类,这样产生的自定义类才能够方便的利用Layer/Model 基类提供的参数管理功能,同时也能够与其他的标准网络层类交互使用。
自定义网络层
对于自定义的网络层,需要实现初始化__init__方法和前向传播逻辑call 方法。
class MyDense(layers.Layer):
# 自定义网络层
def __init__(self, inp_dim, outp_dim):
super(MyDense, self).__init__()
# 创建权值张量并添加到类管理列表中,设置为需要优化
self.kernel = self.add_variable('w', [inp_dim, outp_dim],
trainable=True)
self.add_variable 会返回此张量的python 引用,而变量名name 由
TensorFlow 内部维护,使用的比较少。我们可以通过
In [5]: net = MyDense(4,3) # 创建输入为4,输出为3 节点的自定义层
net.variables,net.trainable_variables
自定义网络
创建自定义网络类,首先创建并继承Model 基类,分别创建对应的网络层对象:
class MyModel(keras.Model):
# 自定义网络类,继承自Model 基类
def __init__(self):
super(MyModel, self).__init__()
# 完成网络内需要的网络层的创建工作
self.fc1 = MyDense(28*28, 256)
self.fc2 = MyDense(256, 128)
self.fc3 = MyDense(128, 64)
self.fc4 = MyDense(64, 32)
self.fc5 = MyDense(32, 10)
然后实现自定义网络的前向运算逻辑:
def call(self, inputs, training=None):
# 自定义前向运算逻辑
x = self.fc1(inputs)
x = self.fc2(x)
x = self.fc3(x)
x = self.fc4(x)
x = self.fc5(x)
return x
模型乐园
对于常用的网络模型,如ResNet,VGG 等,不需要手动创建网络,可以直接从keras.applications 子模块下一行代码即可创建并使用这些经典模型,同时还可以通过设置weights 参数加载预训练的网络参数,非常方便。
加载模型
以 ResNet50 迁移学习为例,一般将ResNet50 去掉最后一层后的网络作为新任务的特征提取子网络,即利用ImageNet 上面预训练的特征提取方法迁移到我们自定义的数据集上,并根据自定义任务的类别追加一个对应数据类别数的全连接分类层,从而可以在预训练网络的基础上可以快速、高效地学习新任务。
# 加载ImageNet 预训练网络模型,并去掉最后一层
resnet = keras.applications.ResNet50(weights='imagenet',include_top=False)
resnet.summary()
# 测试网络的输出
x = tf.random.normal([4,224,224,3])
out = resnet(x)
out.shape
上述代码自动从服务器下载模型结构和在ImageNet 数据集上预训练好的网络参数,由于去掉最后一层,网络的输出大小为[b, 7,7,2048]。
In [6]:
# 新建池化层
global_average_layer = layers.GlobalAveragePooling2D()
In [7]:
# 新建全连接层
fc = layers.Dense(100)
# 重新包裹成我们的网络模型
mynet = Sequential([resnet, global_average_layer, fc])
mynet.summary()
通过设置resnet.trainable = False 可以选择冻结ResNet 部分的网络参数,只训练新建的网络层,从而快速、高效完成网络模型的训练。
测量工具
在网络的训练过程中,经常需要统计准确率,召回率等信息,除了可以通过手动计算并平均方式获取统计数据外,Keras 提供了一些常用的测量工具keras.metrics,专门用于统计训练过程中需要的指标数据。
Keras 的测量工具的使用一般有4 个基本操作流程:新建测量器,写入数据,读取统计数据和清零测量器。
新建测量器
在 keras.metrics 模块下,提供了较多的常用测量类,如统计平均值的Mean 类,统计准确率的Accuracy 类,统计余弦相似度的CosineSimilarity 类等等。下面我们以统计误差值为例,在前向运算时,我们会得到每一个batch 的平均误差,但是我们希望统计一个epoch 的平均误差,因此我们选择使用Mean 测量器:
# 新建平均测量器,适合Loss 数据
loss_meter = metrics.Mean()
写入数据
通过测量器的update_state 函数可以写入新的数据:
# 记录采样的数据
loss_meter.update_state(float(loss))
上述采样代码放置在每个batch 运算完成后,测量器会自动根据采样的数据来统计平均值。
读取统计信息
在采样多次后,可以测量器的result()函数获取统计值:
# 打印统计的平均loss
print(step, 'loss:', loss_meter.result())
清除
由于测量器会统计所有历史记录的数据,因此在合适的时候有必要清除历史状态。通过reset_states()即可实现。例如,在每次读取完平均误差后,清零统计信息,以便下一轮统计的开始:
if step % 100 == 0:
# 打印统计的平均loss
print(step, 'loss:', loss_meter.result())
loss_meter.reset_states() # 清零测量器
可视化
TensorFlow 提供了一个专门的可视化工具,叫做TensorBoard,他通过TensorFlow 将监控数据写入到文件系统,并利用Web 后端监控对应的文件目录,从而可以允许用户从远程查看网络的监控数据。
TensorBoard 的使用需要训练部分和浏览器交互工作。
模型端
在模型端,需要创建写入监控数据的Summary 类,并在需要的时候写入监控数据。首先通过tf.summary.create_file_writer 创建监控对象,并制定监控数据的写入目录:
# 创建监控类,监控数据将写入log_dir 目录
summary_writer = tf.summary.create_file_writer(log_dir)
我们以监控误差数据和可视化图片数据为例,介绍如何写入监控数据。在前向计算完成后,对于误差这种标量数据,我们通过tf.summary.scalar 函数记录监控数据,并指定时间戳step:
with summary_writer.as_default():
# 当前时间戳step 上的数据为loss,写入到ID 位train-loss 对象中
tf.summary.scalar('train-loss', float(loss), step=step)
需要注意的是,TensorBoard 通过字符串ID 来区分不同类别的监控数据,因此对于误差数据,我们将它命名为”train-loss”,其他类的数据不可写入此对象,防止数据污染。
对于图片类型的数据,通过tf.summary.image 函数写入监控图片数据:
with summary_writer.as_default():
# 写入测试准确率
tf.summary.scalar('test-acc', float(total_correct/total),step=step)
# 可视化测试用的图片,设置最多可视化9 张图片
tf.summary.image("val-onebyone-images:", val_images,max_outputs=9, step=step)
运行模型程序后,相应的数据将写入到指定文件目录中。
浏览器端
在运行程序时,通过运行tensorboard --logdir path 指定Web 后端监控的文件目录path.
此时打开浏览器,并输入网址http://localhost:6006 (也可以通过IP 地址远程访问,具体端口可能会变动,可查看命令提示) 即可监控网络训练进度。
在监控页面的上端可以选择不同类型数据的监控页面,比如标量监控页面SCALARS,图片可视化页面IMAGES ,在HISTOGRAMS 页面即可查看张量的直方图等等。
除了监控标量数据和图片数据外,TensorBoard 还支持通过tf.summary.histogram 查看张量的数据直方图分布,以及通过tf.summary.text 打印文本信息:
with summary_writer.as_default():
# 当前时间戳step 上的数据为loss,写入到ID 位train-loss 对象中
tf.summary.scalar('train-loss', float(loss), step=step)
# 可视化真实标签的直方图分布
tf.summary.histogram('y-hist',y, step=step)
# 查看文本信息
tf.summary.text('loss-text',str(float(loss)))