keras concatenate_Tensorflow笔记:高级封装——Keras

009aa485ba4ad2e3dbdfe5d4bf3486c7.png

前言

之前在《Tensorflow笔记:高级封装——tf.Estimator》中介绍了Tensorflow的一种高级封装,本文介绍另一种高级封装Keras。Keras的特点就是两个字——简单,不用花时间和脑子去研究各种细节问题。

1. 贯序结构

最简单的情况就是贯序模型,就是将网络层一层一层堆叠起来,比如DNN、LeNet等,与之相对的非贯序模型的层和层之间可能存在分叉、合并等复杂结构。下面通过一个LeNet的例子来展示Keras如何实现贯序模型,我们依然采用MNIST数据集举例:

862980bc82fd50b074e62cb463c60736.png
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网络的基础结构):

fb917568dd5c7a9d8da62454dde8dbec.png
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,来实现不同的并行策略:

  • tf.distribute.MirroredStrategy:单机多卡情况,每一个GPU都保存变量副本。
  • tf.distribute.experimental.CentralStorageStrategy:单机多卡情况,GPU不保存变量副本,变量都保存在CPU上。
  • tf.distribute.experimental.MultiWorkerMirroredStrategy :在所有机器的每台设备上创建模型层中所有变量的副本。它使用CollectiveOps,一个用于集体通信的 TensorFlow 操作,来聚合梯度并使变量保持同步。
  • tf.distribute.experimental.TPUStrategy:在TPU上训练模型
  • tf.distribute.experimental.ParameterServerStrategy:本例中采用的策略,有专门的ps机负责处理变量和梯度,worker机专门负责训练,计算梯度。所以只有在这种策略下,才需要在os.environ['TF_CONFIG']中设置ps机
  • tf.distribute.OneDeviceStrategy:用单独的设备来训练。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值