侧边栏壁纸
  • 累计撰写 244 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论
隐藏侧边栏

TensorFlow 2 深度神经网络

kaixindeken
2021-09-23 / 0 评论 / 0 点赞 / 82 阅读 / 8,044 字

VGG 网络结构

VGG 是 Visual Geometry Group 的简称,前者代表牛津大学工程科学系的视觉几何课题组。2014 年,VGG 在 ImageNet ILSVRC-2014 分类任务中拿下了第二名的成绩。

简单来讲,VGG 采用了卷积神经网络结构,分为分为如下图所示的 VGG16 和 VGG19。二者在结构上区别不大,只是 VGG19 更深一些,效果略好于 VGG16。为了减少训练时间,本次实验尝试构建 VGG16 网络。

截屏20210923 下午6.00.26.png

结构图从下往上看,VGG 总共包含 12 个卷积层,5 个池化层,以及 3 个全连接层。

VGG 的特点在于卷积层和池化层均采用相同的卷积核(3 \times 33×3)和池化核参数(2 \times 22×2)。同时,网络由多个卷积池化单元块组成,具体来说就是若干个卷积层加上一个池化层组合。

VGG 模型构建

首先,使用 Keras 顺序模型来构建 VGG16 网络。之前的顺序模型使用过程中,我们是先使用 tf.keras.Sequential() 定义一个顺序模型空结构,然后使用 add 操作向其中添加层。

对于复杂的网络结构,你也可以取消 add 操作,直接将网络层以列表的形式罗列在顺序模型中即可。

import tensorflow as tf

model_vgg = tf.keras.Sequential([
    tf.keras.layers.Conv2D(64, (3, 3), input_shape=(224, 224, 3), padding='same', activation='relu'),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
    tf.keras.layers.Conv2D(128, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Conv2D(256, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.Conv2D(256, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.Conv2D(256, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Conv2D(512, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.Conv2D(512, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.Conv2D(512, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Conv2D(512, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.Conv2D(512, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.Conv2D(512, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(4096, activation='relu'),
    tf.keras.layers.Dense(4096, activation='relu'),
    tf.keras.layers.Dense(1000, activation='softmax')
])

model_vgg.summary()

通过 model.summary() 输出模型详情,你可以与下方更详细的 VGG16 模型结构图进行对比,确认是否完全一致。

截屏20210923 下午6.03.17.png

Fashion-MNIST 分类

VGG 本身是为 ImageNet 分类任务设计,ImageNet 是一个包含数万张图片的大型基准数据集,总共有 1000 个目标输出。所以,你也可以看出 VGG16 最终输出层为 1000。不过,由于 ImageNet 数据集体积太大,本次实验我们选择一个较小的 Fashion-MNIST 时尚物品分类数据集。

Fashion-MNIST 时尚物品数据集包含 70,000 张图片,其中训练集为 60,000 张 28x28 像素灰度图像,测试集为 10,000 同规格图像,总共 10 类时尚物品标签。

截屏20210923 下午6.04.10.png

我们使用 TensorFlow 直接加载该数据集。由于数据集托管在外网服务器上,国内的下载速度较慢,你可以通过运行下面的单元格从蓝桥云课服务器上下载数据集。

wget -nc "http://labfile.oss.aliyuncs.com/courses/1211/fashion-mnist.zip"
mkdir -p "/root/.keras/datasets/fashion-mnist/"
unzip -o "fashion-mnist.zip" -d "/root/.keras/datasets/fashion-mnist"

读取数据之后,由于是灰度图像,可以直接除以 255 进行归一化。归一化是一个很常用的处理步骤,能够在一定程度上提高梯度下降法求解最优解的速度,也能够改善最终的分类准确度。

import tensorflow as tf
import numpy as np

(X_train, y_train), (X_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()

# 对特征进行归一化处理
X_train = X_train / 255
X_test = X_test / 255
# 对标签进行独热编码
y_train = np.eye(10)[y_train] 
y_test = np.eye(10)[y_test]

X_train.shape, X_test.shape, y_train.shape, y_test.shape

你可以看到,训练集的形状为 28 \times 2828×28,这也是 Fashion-MNIST 默认图像尺寸。我们可视化训练集第一个样本查看:

from matplotlib import pyplot as plt
%matplotlib inline

plt.imshow(X_train[0], cmap=plt.cm.gray)

数据不变,修改网络

那么,问题来了。VGG16 一开始是为 ImageNet 任务设计,输入形状 input_shape=(224, 224, 3)。其中,224 代表图片尺寸,3 表示彩色图片的 RGB 通道。而 Fashion-MNIST 图片的默认尺寸是 28,且由于是灰度图像只有 1 个通道。

此外,VGG 默认是对 ImageNet 数据集完成 1000 类图像的分类预测,而 Fashion-MNIST 却只有 10 个类别。所以,我们需要修改网络结构如下。

model_mnist = tf.keras.Sequential([
    tf.keras.layers.Conv2D(64, (3, 3), input_shape=(28, 28, 1), padding='same', activation='relu'),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
    tf.keras.layers.Conv2D(128, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Conv2D(256, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.Conv2D(256, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.Conv2D(256, (3, 3), activation='relu', padding='same',),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(4096, activation='relu'),
    tf.keras.layers.Dense(4096, activation='relu'),
    tf.keras.layers.Dense(10, activation='softmax')
])

model_mnist.summary()

修改时,我们一并去除了网络中最后两个卷积池化单元。首先,因为输入尺寸变小,原完整网络已不再适应,会出现尺寸报错。此外,原 VGG 网络太深,训练时需要耗费数小时,所以降低了网络复杂度。

接下来,我们编译模型并开始训练过程。由于指定输出尺寸为 (28, 28, 1),所以还需要通过 reshape 操作向原始数据中补充一个通道维度。

# 对图像形状进行处理,增加一个通道维度
X_train = tf.reshape(X_train, [-1, 28, 28, 1])
X_test = tf.reshape(X_test, [-1, 28, 28, 1])

X_train.shape, X_test.shape, y_train.shape, y_test.shape
# 编译模型
model_mnist.compile(optimizer=tf.optimizers.Adam(),
                    loss='categorical_crossentropy', metrics=['accuracy'])
# 训练模型
history = model_mnist.fit(X_train, y_train, epochs=3, batch_size=32,
                          validation_data=(X_test, y_test))

随着训练迭代,模型的分类准确度会稳步提升。

网络不变,修改数据

上方我们采用的策略是数据基本不变,修改网络结构。实际上,我们也可以完整沿用 VGG 网络结构,修改数据尺寸以配合网络输入。

为了能够输入 VGG16,我们需要对图像进一步处理。首先,需要拓展尺寸到 224,此外复制灰度通道让其变成 RGB 图像。还好,TensorFlow 提供了完善的图像预处理 API,你可以在 tf.image 模块下找到。

# 对图像进行 Resize 操作,变为 224 尺寸
X_train_demo = tf.image.resize(X_train[:100], (224, 224))
X_test_demo = tf.image.resize(X_test[:100], (224, 224))
# 将单通道灰度图像转换为 3 通道 RGB 图像
X_train_demo = tf.image.grayscale_to_rgb(X_train_demo)
X_test_demo = tf.image.grayscale_to_rgb(X_test_demo)

X_train_demo.shape, X_test_demo.shape

由于 tf.image.resize 操作会消耗大量内存,为了避免环境内存报错,上方只示例处理了 100 条数据。此时,输入样本的尺寸就满足 VGG 网络要求了,变成了 (224, 224, 3)。

另外,上方我们给 mnist_model 网络定义了新的输出层以满足 10 个类别输出需求。实际上,有时候也可以直接修改原网络,将 1000 替换为 10。这里将涉及 Keras 顺序模型到函数式模型的转换技巧,希望大家能了解。

首先,model_vgg.layers[-2].output 可以将原顺序模型倒数层提取出来,然后作为新的 tf.keras.layers.Dense(10, activation='softmax') 输出层的输入。此时,我们需要使用 Keras 函数式 API 来重构模型,输入依旧是原顺序模型的输入层,输出则是新的输出层。

mnist_out = tf.keras.layers.Dense(10, activation='softmax')(model_vgg.layers[-2].output)
mnist_model_vgg = tf.keras.Model(model_vgg.input, mnist_out)
mnist_model_vgg.summary()

此时,你可以看到原始的 VGG 网络最后一个输出层的输出形状已经变成了 (None, 10),则表示模型可以解决 Fashion-MNIST 10 分类问题了。由于 VGG 原始网络需要的时间过长,这里不再编译模型和训练。

VGG 迁移学习

我们使用 VGG 模型结构完成了 Fashion-MNIST 分类任务。实际上,得益于迁移学习技术的发展,很多时候都不会选择从 0 开始训练一个模型。从 0 开始训练模型有 2 个明显的问题。首先,需要大量的数据才能够保证深度神经网络学习到位。此外,训练过程需要耗费大量的时间和且需要高性能 GPU 环境。

迁移学习,简单来讲就是从以前的任务当中去学习知识或经验,其中常用的是一种叫微调 Fine-tuning 的技术。微调的原理很简单,我们利用在别人在 ImageNet 等大规模数据上训练好的模型,固定部分网络权重,只使用少量数据去迭代优化部分网络,从而让网络快速匹配新的学习任务。

TensorFlow 提供了多种卷积神经网络在大规模数据上训练好的模型,你可以很轻松找到 VGG16 在 ImageNet 上的预训练模型。接下来,我们尝试加载此模型:

wget -nc "http://labfile.oss.aliyuncs.com/courses/1282/vgg16_weights.zip"
unzip -o "vgg16_weights.zip" -d "/root/.keras/models"
vgg16_notop = tf.keras.applications.VGG16(include_top=False, input_shape=(32, 32, 3))
vgg16_notop.summary()

考虑到微调的需要,所以提供了不包含分类器(输出层)的预训练模型,只需要在导入模型时添加 include_top=False 参数即可。此外,该预训练模型支持自定义输出尺寸,但最小为 32。为了保证后续能快速训练,我们在此选择 32 最小值,当然你也可以选择 VGG 原始支持尺寸 224。

你可以看到,这里预训练模型的结构实际上和我们上面构建的原始 VGG 结构一致。此时,我们向模型添加输出层,使之适应 10 个类别 Fashion-MNIST 分类任务。这里会用到上面将顺序模型转换为函数模型的方法。

flatten = tf.keras.layers.Flatten()(vgg16_notop.output) # 将模型 Flatten 展平
outs = tf.keras.layers.Dense(10, activation='softmax')(flatten) # 连接到 10 输出
model_pretrained = tf.keras.models.Model(inputs=vgg16_notop.input, outputs=outs) # 组合为新模型
model_pretrained.summary()

此时,模型结构就变成我们想要的样子了。

微调的特点是我们可以固定大多数预训练层不参与更新,只更新部分层的权重。所以,这里可以通过设置 layer.trainable 状态来使得模型训练时只更新我们添加的定义层参数。

for layer in model_pretrained.layers[:-2]:
    layer.trainable = False  # 固定前面层参数
for layer in model_pretrained.layers[-2:]:
    layer.trainable = True  # 后两层可训练
# 查看修改后每层可训练状态
for layer in model_pretrained.layers:
    print(layer.name, layer.trainable)

接下来,我们还需要对训练数据进行处理。采用之前提到的方法,我们将尺寸为 28 的原始数据变形为 32,且添加 RGB 通道。迁移学习是一个省时,省数据的方法,所以这里只取出 10000 条训练数据,并使用测试集进行评估。

# 对图像进行 Resize 操作,变为 224 尺寸
X_train_lite = tf.image.resize(X_train[:10000], (32, 32))
X_test_lite = tf.image.resize(X_test, (32, 32))
# 将单通道灰度图像转换为 3 通道 RGB 图像
X_train_lite = tf.image.grayscale_to_rgb(X_train_lite)
X_test_lite = tf.image.grayscale_to_rgb(X_test_lite)

y_train_lite = y_train[:10000]
y_test_lite = y_test

X_train_lite.shape, X_test_lite.shape, y_train_lite.shape, y_test_lite.shape

下面开始迭代训练过程。

# 编译模型
model_pretrained.compile(optimizer=tf.optimizers.Adam(),
                         loss='categorical_crossentropy', metrics=['accuracy'])
# 训练模型
history = model_pretrained.fit(X_train_lite, y_train_lite, epochs=10, batch_size=32,
                               validation_data=(X_test_lite, y_test_lite))

你会发现,迁移学习的速度明显比前面更快。

准确度方面,本次迁移学习并没有太大的帮助。原因在于预训练模型使用 ImageNet 数据集中的类别实际上和我们使用的 Fashion-MNIST 差别很大。如果,你要完成一个已存在或近似于 ImageNet 中的物体分类,那么迁移学习是一种非常好的手段。少量的数据,加上较短的时间,就能得到一个效果还不错的模型。

0

评论区