前言
之前在《Tensorflow笔记:高级封装——tf.Estimator》中介绍了Tensorflow的一种高级封装,本文介绍另一种高级封装Keras。Keras的特点就是两个字——简单,不用花时间和脑子去研究各种细节问题。
1. 贯序结构
最简单的情况就是贯序模型,就是将网络层一层一层堆叠起来,比如DNN、LeNet等,与之相对的非贯序模型的层和层之间可能存在分叉、合并等复杂结构。下面通过一个LeNet的例子来展示Keras如何实现贯序模型,我们依然采用MNIST数据集举例:
LeNet-5模型结构
首先假设我们已经读到了数据,对于MNIST数据可以通过官方API直接获取,如果是其他数据可以自行进行数据预处理,由于数据读取内容不是本篇介绍重点,所以不做介绍。
(train_images, train_labels), (valid_images, valid_labels) = tf.keras.datasets.mnist.load_data()
train_images = train_images.reshape(-1, 28, 28, 1)
valid_images = valid_images.reshape(-1, 28, 28, 1)
train_images, valid_images = train_images / 255.0, valid_images / 255.0
最后数据的格式为 (n, height, width, channel) ,数据和标签的dtype分别为float和int,
Keras相比与原生和tf.Estimator相比对于数据type的要求比较友好。
print(train_images.shape)
# (60000, 28, 28)
print(train_labels.shape)
# (60000,)
print(train_images.dtype)
# float64 / float32 都可以
print(train_labels.dtype)
# uint8 / int16 / int32 / int64 等都可以
接下来开始构建模型
import tensorflow as tf
from tensorflow.keras.optimizers import SGD
# 构建模型结构
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(6, (5,5), activation='tanh', input_shape=(28,28,1)),
tf.keras.layers.MaxPooling2D(2,2),
tf.keras.layers.Conv2D(16, (5,5), activation='tanh'),
tf.keras.layers.MaxPooling2D(2,2),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(120, activation='tanh'),
tf.keras.layers.Dense(84, activation='tanh'),
tf.keras.layers.Dense(10, activation='softmax')
])
# 模型编译(告诉模型怎么优化)
model.compile(loss='sparse_categorical_crossentropy', # 损失函数
optimizer=SGD(lr=0.05, decay=1e-6, momentum=0.9, nesterov=True), # 优化器
metrics=['acc']) # 评估指标
对于贯序模型,只需要调用tf.keras.models.Sequential(),他的参数是一个由tf.keras.layers组成的列表,就可以确定一个模型的结构,然后再简单通过model.compile()就可以确定模型关于“如何优化”方面的信息。
很像sklearn的那样简单易用,没有原生tensorflow那种结构和对话的分离,没有必要维护tensor的name。下面看一些怎么开始训练:
history = model.fit(train_images, train_labels, batch_size=32, epochs=1, verbose=1, shuffle=True, validation_data=(valid_images, valid_labels))
就一句fit就解决了!很sklearn。对于evaluate任务也超简单
model.evaluate(test_images, test_labels, verbose=2)
# [0.06203492795133497, 0.9811]
最后对于predict任务,也和sklearn一样
model.predict(test_images)
可见Keras的另一个优势就是,不需要人为的去考虑每一个batch,只需要指定一个batch_size即可,即使是在predict时也可以直接吧全部数据集喂进去。相比之下在原生Tensorflow中要通过一个for循环一个batch一个batch的去sess.run(train_op),就比较麻烦。
2. 复杂结构
贯序模型对于结构复杂的模型,比如层之间出现了分叉、拼接等操作就无法表示了(比如Inception家族)。但是Keras并没有因此放弃,依然是可以很容易的构建复杂结构的网络的。下面来实现一个下图所示的多塔Inception块(该Inception块及其改进是在各种Inception网络的基础结构):
Inception块结构
假设我们在Previous layer处的输入数据的shape为(256, 256, 3),该结构用Keras这样实现:
import tensorflow as tf
# input数据接口
input_img = tf.keras.layers.Input(shape=(256, 256, 3))
# 分支0
tower0 = tf.keras.layers.Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
# 分支1
tower1 = tf.keras.layers.Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
tower1 = tf.keras.layers.Conv2D(64, (3,3), padding="same", activation="relu")(tower1)
# 分支2
tower2 = tf.keras.layers.Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
tower2 = tf.keras.layers.Conv2D(64, (5,5), padding="same", activation="relu")(tower2)
# 分支3
tower3 = tf.keras.layers.MaxPooling2D((3,3), strides=(1,1), padding="same")(input_img)
tower3 = tf.keras.layers.Conv2D(64, (1,1), padding="same", activation="relu")(tower3)
# 拼接output
output = tf.keras.layers.concatenate([tower0, tower1, tower2, tower3], axis=1)
# 把前面的计算逻辑,分别指定input和output,并构建成网络
model = tf.keras.models.Model(inputs=input_img, outputs=output)
这个过程与Tensorflow原生或tf.Estimator中构建网络结构在本质上是类似的,
都是需要表示根据xxx计算xxx,只不过在这里不需要维护name,以及只需要考虑每一层的输入输出即可,十分节省精力。
最后构建网络的步骤中也只需要指定inputs和outouts两格参数,在计算时也依然是从后到前追溯计算的。我们来看一下搭建这个网络结构的结果:
print(model.summary())
# 下面为输出
Model: "model_1"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_1 (InputLayer) [(None, 256, 256, 3) 0
__________________________________________________________________________________________________
conv2d_8 (Conv2D) (None, 256, 256, 64) 256 input_1[0][0]
__________________________________________________________________________________________________
conv2d_10 (Conv2D) (None, 256, 256, 64) 256 input_1[0][0]
__________________________________________________________________________________________________
max_pooling2d_3 (MaxPooling2D) (None, 256, 256, 3) 0 input_1[0][0]
__________________________________________________________________________________________________
conv2d_7 (Conv2D) (None, 256, 256, 64) 256 input_1[0][0]
__________________________________________________________________________________________________
conv2d_9 (Conv2D) (None, 256, 256, 64) 36928 conv2d_8[0][0]
__________________________________________________________________________________________________
conv2d_11 (Conv2D) (None, 256, 256, 64) 102464 conv2d_10[0][0]
__________________________________________________________________________________________________
conv2d_12 (Conv2D) (None, 256, 256, 64) 256 max_pooling2d_3[0][0]
__________________________________________________________________________________________________
concatenate (Concatenate) (None, 1024, 256, 64 0 conv2d_7[0][0]
conv2d_9[0][0]
conv2d_11[0][0]
conv2d_12[0][0]
==================================================================================================
Total params: 140,416
Trainable params: 140,416
Non-trainable params: 0
__________________________________________________________________________________________________
在tf.keras.layers中定义了很多封装好的层,在复杂的网络,只要用到的都是其中的层,就能通过tf.keras实现。对于那些这里面没有包含的层结构,就只能通过原生Tensorflow或tf.Estimator来手动搭建了,不过对于99.9%的人来说,能把现有的层灵活运用就已经很好了,研究新的层结构的工作,还是交给搞学术研究的科学家吧。
3. 保存与加载
我在《Tensorflow笔记:模型保存、加载和Fine-tune》中详细讲述了原生Tensorflow保存加载模型的过程,可谓是复杂繁琐。相比之下tf.keras的模型保存加载是非常简单的。先来看看模型的保存:
model.save("./model_h5/test-model.h5")
一行代码搞定,
没有网络结构和变量的分离,保存起来非常容易。如果要加载这个模型也同样是一行代码:
new_model = tf.keras.models.load_model("./model_h5/test-model.h5")
这样加载进来的模型就可以和原模型一模一样,可以直接predict和evaluate,当然也可以看作是热启动继续进行增量训练。
new_model.summary() == model.summary() # True
new_model.predict(test_images) # predict
new_model.evaluate(test_images, test_labels, verbose=2) # evaluate
# 热启动 + 增量训练
new_model.fit(training_images, training_labels, batch_size=32, epochs=1, verbose=1, shuffle=True, validation_data=(test_images, test_labels))
4. 迁移学习
除了可以将预训练好的模型加载进来,进行增量学习以外,tf.keras还可以灵活的选取模型中的部分层组成新的模型,并且冻结模型的部分层,以达到迁移学习的目的。比如我们将刚刚保存好的模型"./model_h5/test-model.h5”作为预训练模型,选取它的卷积层+池化层并冻结,然后在后面拼接上新的全链接层。
# 加载 base 模型
base_model = tf.keras.models.load_model("./model_h5/test-model.h5")
# base_model.summary()
# 构建新的网络结构
flatten = base_model.get_layer("flatten").output # 这个layer的name可以通过 base_model.summary()获取
dense = tf.keras.layers.Dense(256, activation='relu')(flatten)
dense = tf.keras.layers.Dense(128, activation='relu')(dense)
pred = tf.keras.layers.Dense(10, activation='softmax')(dense)
# 将 base_model的输入 -> pred 这段网络结构看作是新的模型
fine_tune_model = tf.keras.models.Model(inputs=base_model.input, outputs=pred)
fine_tune_model.summary()
目前为止,我们就得到了一个新的网络结构,他长这个样子
Model: "model"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_input (InputLayer) [(None, 28, 28, 1)] 0
_________________________________________________________________
conv2d (Conv2D) (None, 24, 24, 6) 156
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 12, 12, 6) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 8, 8, 16) 2416
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 4, 4, 16) 0
_________________________________________________________________
flatten (Flatten) (None, 256) 0
_________________________________________________________________
dense_4 (Dense) (None, 256) 65792
_________________________________________________________________
dense_5 (Dense) (None, 128) 32896
_________________________________________________________________
dense_6 (Dense) (None, 10) 1290
=================================================================
Total params: 102,550
Trainable params: 102,550
Non-trainable params: 0
_________________________________________________________________
然后我们来将前面的卷积层冻结,具体冻结哪几层需要我们来手动指定。
# 冻结前面的卷积层
for i, layer in enumerate(fine_tune_model.layers): # 打印各卷积层的名字
print(i, layer.name)
for layer in fine_tune_model.layers[:6]:
layer.trainable = False
现在一切准备就绪,最后就是编译网络结构,并且进行Fine-tune:
# 新模型编译
fine_tune_model.compile(loss='sparse_categorical_crossentropy', optimizer=SGD(lr=0.05, decay=1e-6, momentum=0.9, nesterov=True), metrics=['acc'])
# Fine-tune
fine_tune_model.fit(training_images, training_labels, batch_size=32, epochs=1, verbose=1, shuffle=True, validation_data=(test_images, test_labels))
tf.keras的Fine-tune也十分简单,把预训练模型加载进来之后,可以根据每一层的名字来将该层的output提取出来(即base_model.get_layer("flatten").output),并且可以直接作为新一层的输入。另外tf.keras会自动管理每一层的名字以及整个网络的inputs和outouts,而且可以直接通过model.summary()或model.layers()获取,避免了原生Tensorflow中原模型作者没有很好管理节点名导致无法获取tensor的情况。
5. 部署
5.1 利用tf.Serving+Docker
前面说了那么多,tf.keras又方便又强大,就差最后一步——部署了。我在《Tensorflow笔记:通过tf.Serving+Docker部署》中介绍了如何将已经导出为saved_model格式的模型,通过tf.Serving+Docker进行部署。对于tf.keras模型来说当然也可以这样,只需要将模型保存为saved_model形式就可以通过tf.Serving+Docker进行部署了,保存为saved_model模式的方法也很简单:
# 保存为 h5 模型
# model.save("./model_h5/test-model.h5")
# 保存为 saved_model
tf.saved_model.save(model, './saved_model_keras/1')
与原生Tensorflow同理,保存为saved_model的时候手动指定保存到版本号路径下,在服务时只需指定./saved_model_keras路径即可,tf.Serving会自动在该路径下挑选最新版本进行服务。
5.2 利用flask搭建服务
当然因为tf.keras模型的加载和预测都非常方便,也可以不用tf.Serving来进行部署。可以直接写一个基于flask的后台脚本,接收请求后调用model.predict(),然后返回就可以了。
import numpy as np
import tensorflow as tf
from flask import Flask
from flask import request
import json
app = Flask(__name__)
# 配置tf运行环境 + 加载模型
graph = tf.get_default_graph()
sess = tf.Session()
set_session(sess)
model = tf.keras.models.load_model("./model_h5/test-model.h5")
# 用于服务的接口
@app.route("/predict", methods=["GET","POST"])
def predict():
data = request.get_json()
if 'values' not in data:
return {'result': 'no input'}
arr = data['values']
global sess
global graph
with graph.as_default():
set_session(sess)
res = np.argmax(model.predict(np.array(arr)), axis=1)
return {'result':res.tolist()}
if __name__ == '__main__':
app.run()
这期间会有一些坑,比如要设置graph,否则会出现graph重复导入的问题;以及要设置Session,否则会出现FailedPreconditionError错误。接下来我们来请求一下试试:
import json
import numpy as np
import tensorflow as tf
from urllib import request
# 通过 urllib.request 进行请求
HOST = "http://127.0.0.1:5000/"
image = training_images[0:5].tolist() # 这里的training_images的格式就是前面训练时training_images的格式
data = json.dumps({'values': image}) # 构造json格式的数据,这里的"values"作为key必须和服务端的"values"相同
req = request.Request(HOST + "predict", headers={"Content-Type": "application/json"}) # 请求
res = request.urlopen(req, data=bytes(data, encoding="utf-8"))
result = json.loads(res.read()) # 字符串转化为字典
print(result)
# 打印结果: {'result': [5, 0, 4, 1, 9]}
关于flask和web服务的内容不是本文重点,所以就不介绍了。至此,tf.keras的内容就介绍完了。
6. 并行训练
6.1 单机多卡
6.1.1 结构并行所谓结构并行,就是对不同的GPU分别计算网络中不同的结构。只需要通过with tf.device_scope('/gpu:0')的方法来手动控制设备,以实现并行。比如对于前面的Inception块的例子中,完全可以改写成
with tf.device_scope('/gpu:0'):
tower0 = tf.keras.layers.Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
with tf.device_scope('/gpu:1'):
tower1 = tf.keras.layers.Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
tower1 = tf.keras.layers.Conv2D(64, (3,3), padding="same", activation="relu")(tower1)
with tf.device_scope('/gpu:2'):
tower2 = tf.keras.layers.Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
tower2 = tf.keras.layers.Conv2D(64, (5,5), padding="same", activation="relu")(tower2)
with tf.device_scope('/gpu:3'):
tower3 = tf.keras.layers.MaxPooling2D((3,3), strides=(1,1), padding="same")(input_img)
tower3 = tf.keras.layers.Conv2D(64, (1,1), padding="same", activation="relu")(tower3)
这样就可以使用CPU与多个GPU进行并行计算网络的不同结构部分。
6.1.2 数据并行所谓数据并行,就是不同的GPU都是计算整个网络的每一个节点,只是处理的数据不同(不同的batch)。这种并行方式不需要手动控制设备,只需要加上一行代码就可以
model = ... # 和前面一样
model = tf.keras.utils.multi_gpu_model(model, gpus=2) # 只需要加上这句
model.compile(...) # 和前面一样
model.fit(...) # 和前面一样
只要在model.compile之前加上multi_gpu_model一行就可以将模型转化为数据并行模式。十分简单,其中gpus指用几个GPU。另外除了这种方法也可以直接指定哪一块GPU进行计算
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "3,5"
上面就是指定第三块和第五块显卡参与训练。
6.2 集群并行
在集群并行中,和原生Tensorflow类似,都需要提供一份“集群名单”以及告诉该机器是集群中的谁。并且在集群中的每一台机器上都运行一次脚本,以启动分布式训练。下面看一个例子
import os
import json
import tensorflow as tf
tf.app.flags.DEFINE_string("job_name", "worker", "ps/worker")
tf.app.flags.DEFINE_integer("task_id", 0, "Task ID of the worker running the train")
os.environ['TF_CONFIG'] = json.dumps({
'cluster': {
‘ps’: ["localhost:2222"],
'worker': ["localhost:2223", "localhost:2224"]
},
'task': {'type': FLAGS.job_name, 'index': FLAGS.task_id}
})
本例采用本地机的两个端口模拟集群中的两个机器,"cluster"表示集群的“名单”信息。"task"表示该机器的信息,"index"表示该机器是"worker"列表中的第几个。
接下来就是训练代码部分了,与单机代码不同的是,只需先声明一个strategy,并且将构造模型部分放在with strategy.scope():中就可以了。下面来看一个例子
# 准备数据
(training_images, training_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data(path="./mnist.npz")
training_images = training_images.reshape(-1, 28, 28, 1)
test_images = test_images.reshape(-1, 28, 28, 1)
training_images, test_images = training_images / 255.0, test_images / 255.0
# 声明分布式 strategy
strategy = tf.distribute.experimental.ParameterServerStrategy()
# 在分布式strategy下构造网络模型
with strategy.scope():
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(6, (5,5), activation='tanh', input_shape=(28,28,1)),
tf.keras.layers.MaxPooling2D(2,2),
tf.keras.layers.Conv2D(16, (5,5), activation='tanh'),
tf.keras.layers.MaxPooling2D(2,2),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(120, activation='tanh'),
tf.keras.layers.Dense(84, activation='tanh'),
tf.keras.layers.Dense(10, activation='softmax')
])
# 模型编译
model.compile(loss='sparse_categorical_crossentropy',
optimizer=SGD(lr=0.05, decay=1e-6, momentum=0.9, nesterov=True),
metrics=['acc'])
# 模型训练
history = model.fit(training_images, training_labels, batch_size=32, epochs=3)
# 保存模型
model.save("./model_h5/dist_model.h5")
以上就是分布式keras训练的方法。实际上可以声明不同的strategy,来实现不同的并行策略:
-
:单机多卡情况,每一个GPU都保存变量副本。tf.distribute.MirroredStrategy
-
:单机多卡情况,GPU不保存变量副本,变量都保存在CPU上。tf.distribute.experimental.CentralStorageStrategy
-
:在所有机器的每台设备上创建模型层中所有变量的副本。它使用tf.distribute.experimental.MultiWorkerMirroredStrategy
,一个用于集体通信的 TensorFlow 操作,来聚合梯度并使变量保持同步。CollectiveOps
-
:在TPU上训练模型tf.distribute.experimental.TPUStrategy
-
:本例中采用的策略,有专门的ps机负责处理变量和梯度,worker机专门负责训练,计算梯度。 所以只有在这种策略下,才需要在os.environ['TF_CONFIG']中设置ps机 。tf.distribute.experimental.ParameterServerStrategy
-
:用单独的设备来训练。tf.distribute.OneDeviceStrategy