TensorFlow 2 深度神经网络
随着网络的复杂度增加,尤其是深度神经网络的应用,高阶 API 的优势就变得更为明显。
VGG 网络结构
本次实验中,我们将还原一个经典的深度神经网络结构 VGG。VGG 是 Visual Geometry Group 的简称,前者代表牛津大学工程科学系的视觉几何课题组。2014 年,VGG 在 ImageNet ILSVRC-2014 分类任务中拿下了第二名的成绩。
简单来讲,VGG 采用了卷积神经网络结构,分为分为如下图所示的 VGG16 和 VGG19。二者在结构上区别不大,只是 VGG19 更深一些,效果略好于 VGG16。为了减少训练时间,本次实验尝试构建 VGG16 网络。
结构图从下往上看,VGG 总共包含 12 个卷积层,5 个池化层,以及 3 个全连接层。
VGG 的特点在于卷积层和池化层均采用相同的卷积核(3×3)和池化核参数(2×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 模型结构图进行对比,确认是否完全一致。
Fashion-MNIST 分类
VGG 本身是为 ImageNet 分类任务设计,ImageNet 是一个包含数万张图片的大型基准数据集,总共有 1000 个目标输出。所以,你也可以看出 VGG16 最终输出层为 1000。不过,由于 ImageNet 数据集体积太大,本次实验我们选择一个较小的 Fashion-MNIST 时尚物品分类数据集。
Fashion-MNIST 时尚物品数据集包含 70,000 张图片,其中训练集为 60,000 张 28x28 像素灰度图像,测试集为 10,000 同规格图像,总共 10 类时尚物品标签。
下面,我们使用 TensorFlow 直接加载该数据集。由于数据集托管在外网服务器上,国内的下载速度较慢,你可以通过运行下面的单元格从蓝桥云课服务器上下载数据集。
# 从蓝桥云课服务器下载数据文件,并存放至线上环境 Keras 默认加载目录
!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×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 上的 预训练模型。接下来,我们尝试加载此模型:
# 从蓝桥云课服务器下载挑战所需预训练模型,并存放至线上环境 Keras 默认加载目录
!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 中的物体分类,那么迁移学习是一种非常好的手段。少量的数据,加上较短的时间,就能得到一个效果还不错的模型。
经典卷积神经网络实现
LeNet-5 神经网络是 Yann LeCun 在 1998 年发明的卷积神经网络结构,早期的论文 中将其用于手写字符识别的实验。下方展示了其模型结构图,相对于之前实现过的 VGG 网络要简单不少。
模型输入张量尺寸为 32,依次经过卷积层 → 平均池化层 → 卷积层 → 平均池化层,最后通过 2 个全连接层得到输出。
本次挑战,我们依旧使用实验中的 Fashion-MNIST 数据集,并完成和实验中一致的预处理步骤。
# 从蓝桥云课服务器下载数据文件,并存放至线上环境 Keras 默认加载目录
!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"
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
接下来,你需要参考 LeNet-5 的网络结构,使用 TensorFlow Keras 对其进行构建,并最终完成 Fashion-MNIST 分类任务。
你需要注意以下几点内容:
LeNet-5 默认 32 输入尺寸不得修改,需对数据尺寸进行匹配。
需要使用到 Fashion-MNIST 全部训练和测试数据。
需要构建一个完全一致的 LeNet-5 模型结构。
挑战中未特别说明的内容,都可以自由选择。
首先,使用 TensorFlow Keras 顺序模型方法构建 LeNet-5 模型结构。
model = tf.keras.Sequential() # 构建顺序模型
#卷积层,6 个 5x5 卷积核,步长为 1,relu 激活,第一层需指定 input_shape
model.add(tf.keras.layers.Conv2D(filters=6, kernel_size=(5, 5), strides=(1, 1),
activation='relu', input_shape=(32, 32, 1)))
#平均池化,池化窗口默认为 2
model.add(tf.keras.layers.AveragePooling2D(pool_size=(2, 2), strides=2))
#卷积层,16 个 5x5 卷积核,步为 1,relu 激活
model.add(tf.keras.layers.Conv2D(filters=16, kernel_size=(
5, 5), strides=(1, 1), activation='relu'))
#平均池化,池化窗口默认为 2
model.add(tf.keras.layers.AveragePooling2D(pool_size=(2, 2), strides=2))
#需展平后才能与全连接层相连
model.add(tf.keras.layers.Flatten())
#全连接层,输出为 120,relu 激活
model.add(tf.keras.layers.Dense(units=120, activation='relu'))
#全连接层,输出为 84,relu 激活
model.add(tf.keras.layers.Dense(units=84, activation='relu'))
#全连接层,输出为 10,Softmax 激活
model.add(tf.keras.layers.Dense(units=10, activation='softmax'))
#查看网络结构
model.summary()
接下来,对输入数据尺寸进行调整,编译模型并完成训练。
#对图像形状进行处理,增加一个通道维度
X_train = tf.reshape(X_train, [-1, 28, 28, 1])
X_test = tf.reshape(X_test, [-1, 28, 28, 1])
#对图像进行 Resize 操作,变为 32s 尺寸
X_train = tf.image.resize(X_train, (32, 32))
X_test = tf.image.resize(X_test, (32, 32))
#编译模型,Adam 优化器,多分类交叉熵损失函数,准确度评估
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
#模型训练及评估
model.fit(X_train, y_train, batch_size=64, epochs=2, validation_data=(X_test, y_test))