使用 tf.keras 训练和提供 ML 模型
精品文章,第一时间送达

转载自:TensorFlow,未经允许不得二次转载
Keras 是一种高级神经网络接口,可以在多个后端上运行。其函数式 API 非常人性化且颇具灵活性,可构建各种应用。一经推出,Keras 便迅速受到青睐。2017 年,Keras API 以 tf.keras 的形式实现与核心 TensorFlow 的集成。虽然 tf.keras 和 Keras 拥有独立代码库,但彼此之间紧密耦合。自 TensorFlow 1.9 起发布更新文档和编程人员指南以来,tf.keras 显然成为以 TensorFlow 构建神经网络时要使用的高级 API。
注:更新文档链接
https://www.tensorflow.org/tutorials/
编程人员指南链接
https://www.tensorflow.org/guide/keras
在这篇文章中,我们会介绍使用 tf.keras 训练、导出及提供神经网络的整个流程。例如,我们将使用 Kaggle Planet 数据集训练卷积神经网络,以预测亚马逊森林卫星图像的标签。我们的目的是说明真实用例的端到端管道。相关代码可通过 github 上的可运行笔记本获取。 请注意,您将需要安装最新版本的 TensorFlow(1.11.0,每日构建版),以根据指示完成全部操作。这只是 pip 安装,且 requirements.txt 文件会在存储区中提供。 或者,您也可以通过 Google Colab 立即运行内容!
注:github 链接
https://github.com/sdcubber/keras-training-serving/blob/master/training-and-serving-with-tf-keras.ipynb
Google Colab 链接
https://colab.research.google.com/github/sdcubber/keras-training-serving/blob/master/training-and-serving-with-tf-keras.ipynb
您可在 Kaggle 下载数据。训练数据由大约 40000 张带标签的亚马逊雨林图像组成,每张图像均与多个标签关联:
注:Kaggle 链接
https://www.kaggle.com/c/planet-understanding-the-amazon-from-space/data
只有一个 “天气” 标签:晴朗、薄雾、多云或局部多云
一个或多个 “地面” 标签:农田、裸地、住宅、道路、水域……

天气标签:多云 地面标签:原始森林,道路
Pandas DataFrame 包含图像名称列、天气标签列和地面标签列,而系统会将这些数据编码为二进制向量。您可前往 github 以 .csv 文件的形式获取相关内容:
注:github 链接
https://github.com/sdcubber/keras-training-serving/blob/master/KagglePlanetMCML.csv

我们希望训练出的模型能够准确预测新图像的这些标签。为此,我们会尝试使用针对天气和地面标签提供两种独立输出的网络。预测天气标签是 多类别分类问题的一个例子, 而地面标签可以建模为 多标签分类 问题。因此,两种输出的损失函数有所不同。训练模型后,我们会使用 TensorFlow Serving 导出和提供模型,,如此一来,我们就可以通过 HTTP 发送请求以获得图像的预测结果。
指定模型
我们将从头开始构建自己的模型¹。我们会采用十分经典的配置,包括一些卷积层、ReLU 激活函数和两个位于顶层的密集分类器:
1 import tensorflow as tf
2 IM_SIZE = 128
3
4 image_input = tf.keras.Input(shape=(IM_SIZE, IM_SIZE, 3), name=’input_layer’)
5
6 # Some convolutional layers
7 conv_1 = tf.keras.layers.Conv2D(32,
8 kernel_size=(3, 3),
9 padding=’same’,
10 activation=’relu’)(image_input)
11 conv_1 = tf.keras.layers.MaxPooling2D(padding=’same’)(conv_1)
12 conv_2 = tf.keras.layers.Conv2D(32,
13 kernel_size=(3, 3),
14 padding=’same’,
15 activation=’relu’)(conv_1)
16 conv_2 = tf.keras.layers.MaxPooling2D(padding=’same’)(conv_2)
17
18 # Flatten the output of the convolutional layers
19 conv_flat = tf.keras.layers.Flatten()(conv_2)
20
21 # Some dense layers with two separate outputs
22 fc_1 = tf.keras.layers.Dense(128,
23 activation=’relu’)(conv_flat)
24 fc_1 = tf.keras.layers.Dropout(0.2)(fc_1)
25 fc_2 = tf.keras.layers.Dense(128,
26 activation=’relu’)(fc_1)
27 fc_2 = tf.keras.layers.Dropout(0.2)(fc_2)
28
29 # Output layers: separate outputs for the weather and the ground labels
30 weather_output = tf.keras.layers.Dense(4,
31 activation=’softmax’,
32 name=’weather’)(fc_2)
33 ground_output = tf.keras.layers.Dense(13,
34 activation=’sigmoid’,
35 name=’ground’)(fc_2)
36
37 # Wrap in a Model
38 model = tf.keras.Model(inputs=image_input, outputs=[weather_output, ground_output])
我们有两个输出层,因此在指定模型时应将这些层以输出列表的形式传递。请注意,天气和地面输出层的激活函数并不相同。很方便的是,Model 实现 tf.keras 时会采用简便的 summary() 方法:

编译模型时,系统会以字典的形式提供两个不同的损失函数,而此字典会将张量名称映射到损失:
1 model.compile(optimizer=’adam’,
2 loss={‘weather’: ‘categorical_crossentropy’,
3 ‘ground’: ‘binary_crossentropy’})
编译模型时,系统会以随机权重对其进行初始化,并允许我们选择优化算法来训练网络。
[1] 正如在 Kaggle 竞赛中所证明的那样,通过大型预训练网络进行迁移学习是取得成功的一大关键。但这里的重点并不是在 Kaggle 中获胜。如需获取有关如何实现绝佳性能的提示,请观看介绍如何处理此数据集的精彩 fast.ai 课程。
注:如何处理此数据集的精彩 fast.ai 链接
http://course.fast.ai/lessons/lesson3.html
模型训练
我们开始训练模型吧!我会在我的笔记本电脑上训练此模型,但这台电脑的内存不够,无法存储整个数据集。处理图像数据时经常会出现这种情况。Keras 提供 model.fit_generator() 方法,而该方法可以使用自定义 Python 生成器从磁盘生成图像以进行训练。不过,从 Keras 2.0.6 开始,我们可以使用 Sequence 对象(而不是生成器)实现安全的多进程处理,这意味着您可以显著提升运行速度并降低 GPU(如果您有)遇到瓶颈的风险。Keras 文档已经提供出色的示例代码,我会稍微自定义一下,以实现下列目的:
让其使用将图像名称映射到标签的 DataFrame
每隔一个周期打乱训练数据
1 import ast
2 import numpy as np
3 import math
4 import os
5 import random
6 from tensorflow.keras.preprocessing.image import
7 img_to_array as img_to_array
8
9 from tensorflow.keras.preprocessing.image import load_img as load_img
def load_image(image_path, size):
10 # data augmentation logic such as random rotations can be added here
11 return img_to_array(load_img(image_path, target_size=(size, size))) / 255.
12
13 class KagglePlanetSequence(tf.keras.utils.Sequence):
14 “””
15 Custom Sequence object to train a model on out-of-memory datasets.
16 “””
17
18 def __init__(self, df_path, data_path, im_size, batch_size, mode=’train’):
19 “””
20 df_path: path to a .csv file that contains columns with image names and labels
21 data_path: path that contains the training images
22 im_size: image size
23 mode: when in training mode, data will be shuffled between epochs
24 “””
25 self.df = pd.read_csv(df_path)
26 self.im_size = im_size
27 self.batch_size = batch_size
28 self.mode = mode
29
30 # Take labels and a list of image locations in memory
31 self.wlabels = self.df[‘weather_labels’].apply(lambda x: ast.literal_eval(x)).tolist()
32 self.glabels = self.df[‘ground_labels’].apply(lambda x: ast.literal_eval(x)).tolist()
33 self.image_list = self.df[‘image_name’].apply(lambda x: os.path.join(data_path, x + ‘.jpg’)).tolist()
34
35 def __len__(self):
36 return int(math.ceil(len(self.df) / float(self.batch_size)))
37
38 def on_epoch_end(self):
39 # Shuffles indexes after each epoch
40 self.indexes = range(len(self.image_list))
41 if self.mode == ‘train’:
42 self.indexes = random.sample(self.indexes, k=len(self.indexes))
43
44 def get_batch_labels(self, idx):
45 # Fetch a batch of labels
46 return [self.wlabels[idx * self.batch_size: (idx + 1) * self.batch_size],
47 self.glabels[idx * self.batch_size: (idx + 1) * self.batch_size]]
48
49 def get_batch_features(self, idx):
50 # Fetch a batch of images
51 batch_images = self.image_list[idx * self.batch_size: (1 + idx) * self.batch_size]
52 return np.array([load_image(im, self.im_size) for im in batch_images])
53
54 def __getitem__(self, idx):
55 batch_x = self.get_batch_features(idx)
56 batch_y = self.get_batch_labels(idx)
57 return batch_x, batch_y
您可以使用此 Sequence 对象(而不是自定义生成器)和 fit_generator() 来训练模型。请注意,您无需提供每个周期的步骤数量,因为 __len__ 方法会实现生成器的相应逻辑。
1 seq = KagglePlanetSequence(‘./KagglePlanetMCML.csv’,
2 ‘./data/train/’,
3 im_size=IM_SIZE,
4 batch_size=32)
此外,tf.keras 让您可以使用所有可用的 Keras 回调,以用于提升训练循环。这些调用非常强大,可提供提前停止、安排学习速率及存储 TensorBoard 文件等选项……在这里,我们将在每个周期结束后使用 ModelCheckPoint 回调来保存模型,如此一来,之后我们便可在有需要时继续训练。默认情况下,系统会存储模型架构、训练配置、优化器状态和权重,以便通过单个文件重新创建整个模型。
我们开始训练模型一个周期:
1 callbacks = [
2 tf.keras.callbacks.ModelCheckpoint(‘./model.h5’, verbose=1)
3 ]
4
5 model.fit_generator(generator=seq,
6 verbose=1,
7 epochs=1,
8 use_multiprocessing=True,
9 workers=4,
10 callbacks=callbacks)
Epoch 1/1
Epoch 00001: saving model to ./model.h5
1265/1265 [==============================] – 941s 744ms/step – loss: 0.8686 – weather_loss: 0.6571 – ground_loss: 0.2115
假设我们想在后期阶段微调模型,只需读取模型文件并继续训练,而无需重新编译:
1 another_model = tf.keras.models.load_model(‘./model.h5’)
2 another_model.fit_generator(generator=seq, verbose=1, epochs=1)
最后,比较好的做法是,实例化 Sequencein 测试模式(即不打乱)并将其用于为整个数据集进行预测,以验证我们的 Sequence 是否有效传递所有数据:
1 test_seq = KagglePlanetSequence(‘./KagglePlanetMCML.csv’,
2 ‘./data/train/’,
3 im_size=IM_SIZE,
4 batch_size=32, mode=’test’)
5 predictions = model.predict_generator(generator=test_seq, verbose=1)
6 len(predictions[1]) == len(df_train) # This is True!
那么,数据集 API 又是什么呢?
tf.data API 是一个功能强大的内容库,让您可以使用来自各种来源的数据并将其传递给 TensorFlow 模型。我们可以使用 tf.data API 而不是 Sequence 对象来训练 tf.keras 模型吗?可以。首先,我们将图像和标签一起序列化为 TFRecord 文件,这是在 TensorFlow 中序列化数据所推荐生成的格式:
1 # Serialize images, together with labels, to TF records
2 def _bytes_feature(value):
3 return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
4
5 tf_records_filename = ‘./data/KagglePlanetTFRecord_{}’.format(IM_SIZE)
6 writer = tf.python_io.TFRecordWriter(tf_records_filename)
7
8 # List of image paths, np array of labels
9 im_list = [os.path.join(‘./data/train’, v + ‘.jpg’) for v in df_train[‘image_name’].tolist()]
10 w_labels_arr = np.array([ast.literal_eval(l) for l in df_train[‘weather_labels’]])
11 g_labels_arr = np.array([ast.literal_eval(l) for l in df_train[‘ground_labels’]])
12
13 # Loop over images and labels, wrap in TF Examples, write away to TFRecord file
14 for i in range(len(df_train)):
15 w_labels = w_labels_arr[i].astype(np.float32)
16 g_labels = g_labels_arr[i].astype(np.float32)
17 im = np.array(img_to_array(load_img(im_list[i], target_size=(IM_SIZE, IM_SIZE))) / 255.)
18
19 example = tf.train.Example(features=tf.train.Features(feature={‘image’: _bytes_feature(im.tostring()),
20 ‘weather_labels’: _bytes_feature(w_labels.tostring()),
21 ‘ground_labels’: _bytes_feature(g_labels.tostring())}))
22
23 writer.write(example.SerializeToString())
24
25 writer.close()
将图像和标签转储到 TFRecord 文件后,我们可以使用 tf.data API 设计另一个生成器。其理念是实例化我们文件中的 TFRecordDataset,并告诉它如何使用 map() 操作来解析序列化数据。
1 featdef = {
2 ‘image’: tf.FixedLenFeature(shape=[], dtype=tf.string),
3 ‘weather_labels’: tf.FixedLenFeature(shape=[], dtype=tf.string),
4 ‘ground_labels’: tf.FixedLenFeature(shape=[], dtype=tf.string)
5 }
6
7 def _parse_record(example_proto, clip=False):
8 “””Parse a single record into image, weather labels, ground labels”””
9 example = tf.parse_single_example(example_proto, featdef)
10 im = tf.decode_raw(example[‘image’], tf.float32)
11 im = tf.reshape(im, (IM_SIZE, IM_SIZE, 3))
