作者 | Himanshu Rawlani
译者 | Monanfei,责编 | 琥珀
出品 | AI科技大本营(id:rgznai100)
2019 年 3 月 6 日,谷歌在 TensorFlow 开发者年度峰会上发布了最新版的 TensorFlow 框架 TensorFlow2.0 。新版本对 TensorFlow 的使用方式进行了重大改进,使其更加灵活和更具人性化。具体的改变和新增内容可以从 TensorFlow 的官网找到,本文将介绍如何使用 TensorFlow2.0 构建和部署端到端的图像分类器,以及新版本中的新增内容,包括:
本教程的所有源代码都已发布到 GitHub 库中,有需要的读者可下载使用。
项目地址:
https://github.com/himanshurawlani/practical_intro_to_tf2
在此之前,需要提前安装 TF nightly preview,其中包含 TensorFlow 2.0 alpha 版本,代码如下:
$ pip install -U --pre tensorflow
1. 使用 TensorFlow Datasets 下载数据并进行预处理
TensorFlow Datasets 提供了一组可直接用于 TensorFlow 的数据集,它能够下载和准备数据,并最终将数据集构建成 tf.data.Dataset 形式。
通过 pip 安装 TensorFlow Datasets 的 python 库,代码如下:
$ pip install tfds-nightly
1.1 下载数据集
TensorFlow Datasets 中包含了许多数据集,按照需求添加自己的数据集。
具体的操作方法可见:
https://github.com/tensorflow/datasets/blob/master/docs/add_dataset.md
如果我们想列出可用的数据集,可以用下面的代码:
import tensorflow_datasets as tfdsprint(tfds.list_builders())
在下载数据集之前,我们最好先了解下该数据集的详细信息,例如该数据集的功能信息和统计信息等。本文将使用 tf_flowers 数据集,该数据集的详细信息可以在 TensorFlow 官网找到,具体内容如下:
对于本文即将使用的 tf_flowers 数据集,其大小为 218MB,返回值为 FeaturesDict 对象,尚未进行分割。由于该数据集尚未定义标准分割形式,我们将利用 subsplit 函数将数据集分割为三部分,80% 用于训练,10% 用于验证,10% 用于测试;然后使用 tfds.load() 函数来下载数据,该函数需要特别注意一个参数 as_supervised,该参数设置为 as_supervised=True,这样函数就会返回一个二元组 (input, label) ,而不是返回 FeaturesDict ,因为二元组的形式更方便理解和使用;接下来,指定 with_info=True ,这样就可以得到函数处理的信息,以便加深对数据的理解,代码如下:
import tensorflow_datasets as tfds
SPLIT_WEIGHTS = (8, 1, 1)splits = tfds.Split.TRAIN.subsplit(weighted=SPLIT_WEIGHTS)(raw_train, raw_validation, raw_test), metadata = tfds.load(name="tf_flowers", with_info=True, split=list(splits),# specifying batch_size=-1 will load full dataset in the memory# batch_size=-1,# as_supervised: `bool`, if `True`, the returned `tf.data.Dataset`# will have a 2-tuple structure `(input, label)` as_supervised=True)
1.2 对数据集进行预处理
从 TensorFlow Datasets 中下载的数据集包含很多不同尺寸的图片,我们需要将这些图像的尺寸调整为固定的大小,并且将所有像素值都进行标准化,使得像素值的变化范围都在 0~1 之间。这些操作显得繁琐无用,但是我们必须进行这些预处理操作,因为在训练一个卷积神经网络之前,我们必须指定它的输入维度。不仅如此,网络中最后全连接层的 shape 取决于 CNN 的输入维度,因此这些预处理的操作是很有必要的。
如下所示,我们将构建函数 format_exmaple(),并将它传递给 raw_train, raw_validation 和 raw_test 的映射函数,从而完成对数据的预处理。需要指明的是,format_exmaple() 的参数和传递给 tfds.load() 的参数有关:如果 as_supervised=True,那么 tfds.load() 将下载二元组 (image, labels) ,该二元组将作为参数传递给 format_exmaple();如果 as_supervised=False,那么 tfds.load() 将下载一个字典 <image,lable> ,该字典将作为参数传递给 format_exmaple() 。
def format_example(image, label): image = tf.cast(image, tf.float32) # Normalize the pixel values image = image / 255.0 # Resize the image image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE)) return image, label
train = raw_train.map(format_example)validation = raw_validation.map(format_example)test = raw_test.map(format_example)
除此之外,我们还对 train 对象调用 .shuffle(BUFFER_SIZE) ,用于打乱训练集的顺序,该操作能够消除样本的次序偏差。Shuffle 的缓冲区大小最后设置得和数据集一样大,这样能够保证数据被充分的打乱。接下来我们要用 .batch(BATCH_SIZE) 来定义这三类数据集的 batch 大小,这里我们将 batch 的大小设置为 32 。最后我们用 .prefetch() 在后台预加载数据,该操作能够在模型训练的时候进行,从而减少训练时间,下图直观地描述了 .prefetch() 的作用。
不采取 prefetch 操作,CPU 和 GPU/TPU 的大部分时间都处在空闲状态
采取 prefetch 操作后,CPU 和 GPU/TPU 的空闲时间显著较少
在该步骤中,有几点值得注意:
上面提到的 .shuffle ()和 .repeat(),可以用 tf.data.Dataset.apply() 中的 tf.data.experimental.shuffle_and_repeat() 来代替:
ds = image_label_ds.apply( tf.data.experimental.shuffle_and_repeat(buffer_size=image_count))ds = ds.batch(BATCH_SIZE)ds = ds.prefetch(buffer_size=AUTOTUNE)
1.3 数据增广
数据增广是提高深度学习模型鲁棒性的重要技术,它可以防止过拟合,并且能够帮助模型理解不同数据类的独有特征。例如,我们想要得到一个能区分“向日葵”和“郁金香”的模型,如果模型只学习了花的颜色从而进行辨别,那显然是不够的。我们希望模型能够理解花瓣的形状和相对大小,是否存在圆盘小花等等。
为了防止模型使用颜色作为主要的判别依据,可以使用黑白图片或者改变图片的亮度参数。为了减小图片拍摄方向导致的偏差,可以随机旋转数据集中的图片,依次类推,可以得到更多增广的图像。
在训练阶段,对数据进行实时增广操作,而不是手动的将这些增广图像添加到数据上。用如下所示的映射函数来实现不同类型的数据增广:
def augment_data(image, label): print("Augment data called!") image = tf.image.random_flip_left_right(image) image = tf.image.random_contrast(image, lower=0.0, upper=1.0) # Add more augmentation of your choice return image, label
train = train.map(augment_data)
1.4 数据集可视化
通过可视化数据集中的一些随机样本,不仅可以发现其中存在的异常或者偏差,还可以发现特定类别的图像的变化或相似程度。使用 train.take() 可以批量获取数据集,并将其转化为 numpy 数组, tfds.as_numpy(train) 也具有相同的作用,如下代码所示:
plt.figure(figsize=(12,12))
for batch in train.take(1): for i in range(9): image, label = batch[0][i], batch[1][i] plt.subplot(3, 3, i+1) plt.imshow(image.numpy()) plt.title(get_label_name(label.numpy())) plt.grid(False) # ORfor batch in tfds.as_numpy(train): for i in range(9): image, label = batch[0][i], batch[1][i] plt.subplot(3, 3, i+1) plt.imshow(image) plt.title(get_label_name(label)) plt.grid(False) # We need to break the loop else the outer loop # will loop over all the batches in the training set break
运行上述代码,我们得到了一些样本图像的可视化结果,如下所示:
2. 用tf.keras 搭建一个简单的CNN模型
tf.keras 是一个符合 Keras API 标准的 TensorFlow 实现,它是一个用于构建和训练模型的高级API,而且对 TensorFlow 特定功能的支持相当好(例如 eager execution 和 tf.data 管道)。 tf.keras 不仅让 TensorFlow 变得更加易于使用,而且还保留了它的灵活和高效。
张量 (image_height, image_width, color_channels) 作为模型的输入,在这里不用考虑 batch 的大小。黑白图像只有一个颜色通道,而彩色图像具有三个颜色通道 (R,G,B) 。在此,我们采用彩色图像作为输入,输入图像尺寸为 (128,128,3) ,将该参数传递给 shape,从而完成输入层的构建。
接下来我们将用一种很常见的模式构建 CNN 的卷积部分:一系列堆叠的 Conv2D 层和 MaxPooling2D 层,如下面的代码所示。最后,将卷积部分的输出((28,28,64)的张量)馈送到一个或多个全连接层中,从而实现分类。
值得注意的是,全连接层的输入必须是一维的向量,而卷积部分的输出却是三维的张量。因此我们需要先将三维的张量展平成一维的向量,然后再将该向量输入到全连接层中。数据集中有 5 个类别,这些信息可以从数据集的元数据中获取。因此,模型最后一个全连接层的输出是一个长度为 5 的向量,再用 softmax 函数对它进行激活,至此就构建好了 CNN 模型。
from tensorflow import keras
# Creating a simple CNN model in keras using functional APIdef create_model(): img_inputs = keras.Input(shape=IMG_SHAPE) conv_1 = keras.layers.Conv2D(32, (3, 3), activation='relu')(img_inputs) maxpool_1 = keras.layers.MaxPooling2D((2, 2))(conv_1) conv_2 = keras.layers.Conv2D(64, (3, 3), activation='relu')(maxpool_1) maxpool_2 = keras.layers.MaxPooling2D((2, 2))(conv_2) conv_3 = keras.layers.Conv2D(64, (3, 3), activation='relu')(maxpool_2) flatten = keras.layers.Flatten()(conv_3) dense_1 = keras.layers.Dense(64, activation='relu')(flatten) output = keras.layers.Dense(metadata.features['label'].num_classes, activation='softmax')(dense_1)
model = keras.Model(inputs=img_inputs, outputs=output) return model
上面的模型是通过 Kearas 的 Functional API 构建的,在 Keras中 还有另一种构建模型的方式,即使用 Model Subclassing API,它按照面向对象的结构来构建模型并定义它的前向传递过程。
2.1 编译和训练模型
在 Keras 中,编译模型就是为其设置训练过程的参数,即设置优化器、损失函数和评估指标。通过调用 model 的 .fit() 函数来设置这些参数,例如可以设置训练的 epoch 次数,再例如直接对 trian 和 validation 调用 .repeat() 功能,并传递给 .fit() 函数,这样就可以保证模型在数据集上循环训练指定的 epoch 次数。
在调用 .fit() 函数之前,我们需要先计算几个相关的参数:
# Calculating number of images in train, val and test setsnum_train, num_val, num_test = (metadata.splits['train'].num_examples * weight/10 for weight in SPLIT_WEIGHTS)steps_per_epoch = round(num_train)//BATCH_SIZEvalidation_steps = round(num_val)//BATCH_SIZE
如上代码所示,由于下载的数据集没有定义标准的分割形式,我们通过设置 8:1:1 的分割比例,将数据集依次分为训练集、验证集和测试验证集。
def train_model(model): model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
# Creating Keras callbacks tensorboard_callback = keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1) model_checkpoint_callback = keras.callbacks.ModelCheckpoint( 'training_checkpoints/weights.{epoch:02d}-{val_loss:.2f}.hdf5', period=5) os.makedirs('training_checkpoints/', exist_ok=True) early_stopping_checkpoint = keras.callbacks.EarlyStopping(patience=5)
history = model.fit(train.repeat(), epochs=epochs, steps_per_epoch=steps_per_epoch, validation_data=validation.repeat(), validation_steps=validation_steps, callbacks=[tensorboard_callback, model_checkpoint_callback, early_stopping_checkpoint]) return history
2.2 可视化训练过程中的评估指标变化
如下图所示,我们将训练集和验证集上的评估指标进行了可视化,该指标为 train_model() 或者 manually_train_model() 的返回值。在这里,我们使用 Matplotlib 绘制曲线图:
训练集和验证集的评估指标随着训练epoch的变化
这些可视化图能让我们更加深入了解模型的训练程度。在模型训练过程中,确保训练集和验证集的精度在逐渐增加,而损失逐渐减少,这是非常重要的。
TensorFlow2.0 可以在 Jupyter notebook 中使用功能齐全的 TensorBoard 。在模型开始训练之前,先启动 TensorBoard ,这样我们就可以在训练过程中动态观察这些评估指标的变化。如下代码所示(注意:需要提前创建 logs/ 文件夹):
%load_ext tensorboard.notebook%tensorboard --logdir logs/
Jupyer notebook 中的TensorBoard 视图
3. 使用预训练的模型
在上一节中,我们训练了一个简单的 CNN 模型,它给出了大约 70% 的准确率。通过使用更大、更复杂的模型,获得更高的准确率,预训练模型是一个很好的选择。预训练模型通常已经在大型的数据集上进行过训练,通常用于完成大型的图像分类任务。直接使用预训练模型来完成我们的分类任务,我们也可以运用迁移学习的方法,只使用预训练模型的一部分,重新构建属于自己的模型。
简单来讲,迁移学习可以理解为:一个在足够大的数据集上经过训练的模型,能够有效地作为视觉感知的通用模型,通过使用该模型的特征映射,我们就可以构建一个鲁棒性很强的模型,而不需要很多的数据去训练。
3.1 下载预训练模型
本次将要用到的模型是由谷歌开发的 InceptionV3 模型,该模型已经在 ImageNet 数据集上进行过预训练,该数据集含有 1.4M 张图像和相应的 1000 个类别。InceptionV3 已经学习了我们常见的 1000 种物体的基本特征,因此,该模型具有强大的特征提取能力。
模型下载时,需要指定参数 include_top=False,该参数使得下载的模型不包含最顶层的分类层,因为我们只想使用该模型进行特征提取,而不是直接使用该模型进行分类。预训练模型的分类模块通常受原始的分类任务限制,如果想将预训练模型用在新的分类任务上,我们需要自己构建模型的分类模块,而且需要将该模块在新的数据集上进行训练,这样才能使模型适应新的分类任务。
from tensorflow import keras
# Create the base model from the pre-trained model MobileNet V2base_model = keras.applications.InceptionV3(input_shape=IMG_SHAPE,# We cannot use the top classification layer of the pre-trained model as it contains 1000 classes.# It also restricts our input dimensions to that which this model is trained on (default: 299x299) include_top=False, weights='imagenet')
我们将预训练模型当做一个特征提取器,输入(128,128,3)的图像,得到(2,2,2048)的输出特征。特征提取器可以理解为一个特征映射过程,最终的输出特征是输入的多维表示,在新的特征空间中,更加利于图像的分类。
3.2 添加顶层的分类层
由于指定了参数 include_top=False,下载的 InceptionV3 模型不包含最顶层的分类层,因此我们需要添加一个新的分类层,而且它是为 tf_flowers 所专门定制的。通过 Keras 的序列模型 API,将新的分类层堆叠在下载的预训练模型之上,代码如下:
def build_model(): # Using Sequential API to stack up the layers model = keras.Sequential([ base_model, keras.layers.GlobalAveragePooling2D(), keras.layers.Dense(metadata.features['label'].num_classes, activation='softmax') ]) # Compile the model to configure training parameters model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) return model
inception_model = build_model()
以上代码理解如下:
值得注意的是,在模型的编译和训练过程中,我们使用 base_model.trainable = False 将卷积模块进行了冻结,该操作可以防止在训练期间更新卷积模块的权重,接下来就可以在 tf_flowers 数据集上进行模型训练了。
3.3 训练顶层的分类层
训练的步骤和上文中 CNN 的训练步骤相同,如下图所示,我们绘制了训练集和验证集的判据指标随训练过程变化的曲线图:
开始训练预训练模型后,训练集和验证集的评估指标随着训练epoch的变化
从图中可以看到,验证集的精度高略高于训练集的精度。这是一个好兆头,说明该模型的泛化能力较好,使用测试集来评估模型可以进一步验证模型的泛化能力。如果想让模型取得更好的效果,对模型进行微调。
3.4 对预训练网络进行微调
在上面的步骤中,我们仅在 InceptionV3 模型的基础上简单训练了几层网络,而且在训练期间并没有更新其卷积模块的网络权重。为了进一步提高模型的性能,对卷积模块的顶层进行微调。在此过程中,卷积模块的顶层和我们自定义的分类层联系了起来,它们都将为 tf_flowers 数据集提供定制化的服务。具体的内容可以参见 TensorFlow 的官网解释。
链接:
https://www.tensorflow.org/alpha/tutorials/images/transfer_learning#fine_tuning
下面的代码将 InceptionV3 的卷积模块顶层进行了解冻,使得它的权重可以跟随训练过程进行改变。由于模型已经发生了改变,不再是上一步的模型了,因此在训练新的模型之前,我们需要对模型重新编译一遍。
# Un-freeze the top layers of the modelbase_model.trainable = True# Let's take a look to see how many layers are in the base modelprint("Number of layers in the base model: ", len(base_model.layers))
# Fine tune from this layer onwardsfine_tune_at = 249
# Freeze all the layers before the `fine_tune_at` layerfor layer in base_model.layers[:fine_tune_at]: layer.trainable = False # Compile the model using a much lower learning rate.inception_model.compile(optimizer = tf.keras.optimizers.RMSprop(lr=0.0001), loss='sparse_categorical_crossentropy', metrics=['accuracy'])
history_fine = inception_model.fit(train.repeat(), steps_per_epoch = steps_per_epoch, epochs = finetune_epochs, initial_epoch = initial_epoch, validation_data = validation.repeat(), validation_steps = validation_steps)
微调的目的是使得模型提取的特征更加适应新的数据集,因此,微调后的模型可以让准确度提高好几个百分点。但是如果我们的训练数据集非常小,并且和 InceptionV3 原始的预训练集非常相似,那么微调可能会导致模型过拟合。如下图所示,在微调之后,我们再次绘制了训练集和验证集的评估指标的变化。
注意:本节中的微调操作是针对预训练模型中的少量顶层卷积层进行的,所需要调节的参数量较少。如果我们将预训练模型中所有的卷积层都解冻了,直接将该模型和自定义的分类层联合,通过训练算法对所有图层进行训练,那么梯度更新的量级是非常巨大的,而且预训练模型将会忘记它曾经学会的东西,那么预训练就没有太大的意义了。
微调模型后,训练集和验证集的评估指标随着训练epoch的变化
从图中可以看到,训练集和验证集的精度都有所提升。我们观察到,在从微调开始的第一个 epoch 结束后,验证集的误差开始上升,但它最终还是随着训练过程而下降了。这可能是因为权重更新得过快,从而导致了震荡。因此,相比于上一步中的模型,微调更加适合较低的学习率。
4. 使用 TensorFlow Serving 为模型发布服务
TensorFlow Serving 能够将模型发布,从而使得我们能够便捷地调用该模型,完成特定环境下的任务。TensorFlow Serving 将提供一个 URL 端点,我们只需要向该端点发送 POST 请求,就可以得到一个 JSON 响应,该响应包含了模型的预测结果。可以看到,我们根本就不用担心硬件配置的问题,一个简单的 POST 请求就可以解决复杂的分类问题。
4.1 安装 TensorFlow Serving
1、添加 TensorFlow Serving的源(一次性设置)
$ echo "deb [arch=amd64] http://storage.googleapis.com/tensorflow-serving-apt stable tensorflow-model-server tensorflow-model-server-universal" | sudo tee /etc/apt/sources.list.d/tensorflow-serving.list && \$ curl https://storage.googleapis.com/tensorflow-serving-apt/tensorflow-serving.release.pub.gpg | sudo apt-key add -
2、安装并更新 TensorFlow ModelServer
$ apt-get update && apt-get install tensorflow-model-server
一旦安装完成,就可以使用如下命令开启 TensorFlow Serving 服务。
$tensorflow_model_server
4.2 将 Keras 模型导出为 SavedModel 格式
为了将训练好的模型加载到 TensorFlow Serving 服务器中,首先我们需要将模型保存为 SavedModel 格式。TensorFlow 提供了 SavedModel 格式的导出方法,该方法简单易用,很快地导出 SavedModel 格式。
下面的代码会在指定的目录中创建一个 protobuf 文件,通过该文件,查询模型的版本号。在实际的使用中,请求服务的版本号,TensorFlow Serving 将会为我们选择相应版本的模型进行服务。每个版本的模型都会导出到相应的子目录下。
from tensorflow import keras
# '/1' specifies the version of a model, or "servable" we want to exportpath_to_saved_model = 'SavedModel/inceptionv3_128_tf_flowers/1'
# Saving the keras model in SavedModel formatkeras.experimental.export_saved_model(inception_model, path_to_saved_model)
# Load the saved keras model backrestored_saved_model = keras.experimental.load_from_saved_model(path_to_saved_model)
4.3 启动 TensorFlow Serving 服务器
在本地启动 TensorFlow Serving 服务器,可以使用如下代码:
$ tensorflow_model_server --model_base_path=/home/ubuntu/Desktop/Medium/TF2.0/SavedModel/inceptionv3_128_tf_flowers/ --rest_api_port=9000 --model_name=FlowerClassifier
Failed to start server. Error: Invalid argument: Expected model ImageClassifier to have an absolute path or URI; got base_path()=./inceptionv3_128_tf_flowers
4.4 向TensorFlow服务器发送 REST请求
TensorFlow ModelServer 支持 RESTful API。我们需要将预测请求作为一个 POST,发送到服务器的 REST 端点。在发送 POST 请求之前,先加载示例图像,并对它做一些预处理。
TensorFlow Serving 服务器的期望输入为(1,128,128,3)的图像,其中,"1" 代表 batch 的大小。通过使用 Keras 库中的图像预处理工具,能够加载图像并将其转化为指定的大小。
服务器 REST 端点的 URL 遵循以下格式:
http://host:port/v1/models/${MODEL_NAME}[/versions/${MODEL_VERSION}]:predict
其中,/versions/${MODEL_VERSION} 是一个可选项,用于选择服务的版本号。下面的代码先加载了输入图像,并对其进行了预处理,然后使用上面的 REST 端点发送 POST 请求:
import json, requestsfrom tensorflow.keras.preprocessing.image import img_to_array, load_imgimport numpy as np
image_path = 'sunflower.jpg'# Loading and pre-processing our input imageimg = image.img_to_array(image.load_img(image_path, target_size=(128, 128))) / 255.img = np.expand_dims(img, axis=0)payload = {"instances": img.tolist()}
# sending post request to TensorFlow Serving serverjson_response = requests.post('http://localhost:9000/v1/models/FlowerClassifier:predict', json=payload)pred = json.loads(json_response.content.decode('utf-8'))
# Decoding the response using decode_predictions() helper function# You can pass "k=5" to get top 5 predicitonsget_top_k_predictions(pred, k=3)
代码的输出如下:
Top 3 predictions:[('sunflowers', 0.978735), ('tulips', 0.0145516), ('roses', 0.00366251)]
5. 总结
最后对本文的要点简单总结如下:
相关链接:
https://medium.com/@himanshurawlani/getting-started-with-tensorflow-2-0-faf5428febae
(*本文为 AI科技大本营编译文章,转载请联系原作者)
◆
CTA核心技术及应用峰会
◆
5月25-27日,由中国IT社区CSDN与数字经济人才发展中心联合主办的第一届CTA核心技术及应用峰会将在杭州国际博览中心隆重召开,峰会将围绕人工智能领域,邀请技术领航者,与开发者共同探讨机器学习和知识图谱的前沿研究及应用。
更多重磅嘉宾请识别海报二维码查看,点击阅读原文即刻抢购。添加小助手微信15101014297,备注“CTA”,了解票务以及会务详情。
推荐阅读
点击阅读原文,了解「CTA核心技术及应用峰会」