深度学习之卷积神经网络(4)LeNet-5实战

深度学习之卷积神经网络(4)LeNet-5实战


 1990年代,Yann LeCun等人提出了用于手写数字和机器打印字符图片识别的神经网络,被命名为LetNet-5 [1]。LetNet-5的提出,使得卷积神经网络在当时能够成功被商用,广泛应用在邮政编码、支票号码识别任务中。下图是LetNet-5的网络结构图,它接受 32 × 32 32×32 32×32大小的数字、字符图片,经过第一个卷积层得到 [ b , 28 , 28 , 6 ] [b,28,28,6] [b,28,28,6]形状的张量,经过一个向下采样层,张量尺寸缩小到 [ b , 14 , 14 , 6 ] [b,14,14,6] [b,14,14,6],经过第二个卷积层,得到 [ b , 10 , 10 , 16 ] [b,10,10,16] [b,10,10,16]形状的张量,同样经过下采样层,张量尺寸缩小到 [ b , 5 , 5 , 16 ] [b,5,5,16] [b,5,5,16],在经过全连接层之前,先将张量打成 [ b , 400 ] [b,400] [b,400]的张量,送入输出节点数分别为120、84的两个全连接层,得到 [ b , 84 ] [b,84] [b,84]的张量,最后通过Gaussian connection层。

[1] Y. Lecun, L. Bottou, Y. Bengio 和 P. Haffner, “Gradient-based learning applied to document recognition,” 出处 Proceedings of the IEEE, 1998.

深度学习之卷积神经网络(4)LeNet-5实战

LeNet-5网络结构


 现在看来,LeNet-5网络层数较少(2个卷积层和2个全连接层),参数量较少,计算代价较低,尤其在现代GPU的加持下,数分钟即可训练好LeNet-5网络。

 我们在LeNet-5的基础上进行了少许调整,使得它更容易在现代深度学习框架上实现。首先我们将输入 X \boldsymbol X X形状由 32 × 32 32×32 32×32调整为 28 × 28 28×28 28×28,然后将2个下采样层实现为最大池化层(降低特征图的高、宽,后面会介绍),最后利用全连接层替换掉Gaussian connection层。下文统一称修改的网络也为LeNet-5网络。网络结构图如图所示:

深度学习之卷积神经网络(4)LeNet-5实战

手写数字图片识别模型结构

加载数据集


 我们基于MNIST手写数字图片数据集训练LeNet-5网络,并测试其最终准确度。前面已经介绍了如何在TensorFlow中加载MNIST数据集。详见深度学习(18)神经网络与全连接层一: 数据加载

# 加载MNIST数据集
(x, y), (x_test, y_test) = keras.datasets.mnist.load_data()
# 转换数据类型
# x: [0~255] => [0~1.]
x = tf.convert_to_tensor(x, dtype=tf.float32) / 255.
y = tf.convert_to_tensor(y, dtype=tf.int32)
x_test = tf.convert_to_tensor(x_test, dtype=tf.float32) / 255.
y_test = tf.convert_to_tensor(y_test, dtype=tf.int32)
# 创建数据集
train_db = tf.data.Dataset.from_tensor_slices((x, y)).batch(128)
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(128)

创建网络


 首先通过Sequential容器创建LeNet-5,代码如下:

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential, losses, optimizers, datasets


network = Sequential([  # 网络容器
    layers.Conv2D(6, kernel_size=3, strides=1),  # 第一个卷积层,6个3×3卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Conv2D(16, kernel_size=2, strides=1),  # 第二个卷积层,16个3×3卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Flatten(),  # 打平层,方便全连接层处理

    layers.Dense(120, activation='relu'),  # 全连接层,120个节点
    layers.Dense(84, activation='relu'),  # 全连接层,84个节点
    layers.Dense(10),  # 全连接层,10个节点
])

# build一次网格模型,给输入x的形状,其中4为随意给的batchsize
network.build(input_shape=(4, 28, 28, 1))
# 统计网格信息
network.summary()


运行结果如下图所示:

深度学习之卷积神经网络(4)LeNet-5实战


 通过summary()函数统计出每层的参数量,打印出网格结构信息和每层参数数量详情,如下表所示,我们可以与全连接网络的参数量表进行比较。

网络参数量统计表
卷积层1 卷积层2 全连接层1 全连接层2 全连接层3
参数量 60 880 48120 10164 850


可以看到,卷积层的参数量非常少,主要的参数量集中在全连接层。由于卷积层将输入层特征维度降低很多,从而使得全连接层的参数量不至于过大,整个模型的参数量约60K,而全连接网络参数量达到了34万个,因此通过卷积神经网络可以显著降低网络参数量,同时增加网络深度。

训练阶段


 在训练阶段,首先将数据集中shape为 [ b , 28 , 28 ] [b,28,28] [b,28,28]的输入 X \boldsymbol X X增加一个维度,调整shape为 [ b , 28 , 28 , 1 ] [b,28,28,1] [b,28,28,1],送入模型进行前向计算,得到输出张量output,shape为 [ b , 10 ] [b,10] [b,10]。我们新建交叉熵损失函数类(没错,损失函数也能使用类方式)用于处理分类任务,通过设定from_logits=True标志位将softmax激活函数实现在损失函数中,不需要手动添加损失函数,提升数值计算稳定性。代码如下:

from tensorflow.keras import layers, Sequential, losses, optimizers, datasets
# 创建损失函数的类,在实际计算时直接调用实例即可
criteon = losses.CategoricalCrossentropy(from_logits=True)


训练部分实现如下:

# 训练部分实现如下
# 构建梯度记录环境
with tf.GradientTape as tape:
    # 插入通道维度,=>[b,28,28,1]
    x = tf.expand_dims(x, axis=3)
    # 向前计算,获得10类别的概率分布,[b,784] => [b,10]
    out = network(x)
    # 真实标签one-hot编码,[b] => [b,10]
    y_onehot = tf.one_hot(y, depth=10)
    # 计算交叉熵损失函数,标量
    loss = criteon(y_onehot, out)


获得损失值后,通过TensorFlow的梯度记录器tf.GradientTape()来计算损失函数loss对网络参数network.trainable_variables之间的梯度,并通过optimizer对象自动更新网络权值参数。代码如下:

# 自动计算梯度
grads = tape.gradient(loss, network.trainable_variables)
# 自动更新参数
optimizer.apply_gradients(zip(grads, network.trainable_variables))


重复上述步骤若干次后即可完成训练工作

测试阶段


 在测试阶段,由于不需要记录梯度信息,代码一般不需要写在with tf.GradientTape() as tape环境中。前向计算得到的输出经过softmax函数后,代表了网络预测当前图片输入 x \boldsymbol x x属于类别i的概率 P ( x 标 签 是 i │ x ) , i ∈ [ 0 , 9 ] P(x标签是i│x),i∈[0,9] P(x标签是i│x),i∈[0,9]。通过argmax函数选取概率最大的元素所在的索引,作为当前x的预测类别,与真实标注y比较,通过计算比较结果中间True的数量并求和来统计预测正确的样本的个数,最后除以总样本的个数,得出网络的测试准确度。代码如下:

# 记录预测正确的数量,总样本数量
correct, total = 0, 0
for x, y in test_db:  # 遍历所有训练集样本
    # 插入通道维度,=>[b,28,28,1]
    x = tf.expand_dims(x, axis=3)
    # 向前计算,获得10类别的概率分布,[b,784] => [b,10]
    out = network(x)
    # 真实的流程时先经过softmax,再argmax
    # 但是由于softmax不改变元素的大小相对关系,故省去
    pred = tf.argmax(out,axis=-1)
    y = tf.cast(y, tf.int64)
    # 统计预测样本总数
    correct += float(tf.reduce_sum(tf.cast(tf.equal(pred, y), tf.float32)))
    # 统计预测样本总数
    total += x.shape[0]
# 计算准确率
print('test acc:', correct/total)


 在数据集上面循环训练30个Epoch后,网络的训练准确度达到了98.1%,测试准确度也达到了97.7%。对于非常简单的手写数字识别任务,古老的LeNet-5网络已经可以取得很好地效果,但是稍微复杂一点的任务,比如色彩动物图片识别,LeNet-5性能就会急剧下降。

完整代码

import os

from Chapter08 import metrics
from Chapter08.metrics import loss_meter

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'


import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential, losses, optimizers, datasets

# 加载MNIST数据集
def preprocess(x, y):
    # 预处理函数
    x = tf.cast(x, dtype=tf.float32) / 255
    y = tf.cast(y, dtype=tf.int32)

    return x, y


# 加载MNIST数据集
(x, y), (x_test, y_test) = keras.datasets.mnist.load_data()
# 创建数据集
batchsz = 128
train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.map(preprocess).shuffle(60000).batch(batchsz).repeat(10)
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.batch(batchsz)

network = Sequential([  # 网络容器
    layers.Conv2D(6, kernel_size=3, strides=1),  # 第一个卷积层,6个3×3卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Conv2D(16, kernel_size=2, strides=1),  # 第二个卷积层,16个3×3卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Flatten(),  # 打平层,方便全连接层处理

    layers.Dense(120, activation='relu'),  # 全连接层,120个节点
    layers.Dense(84, activation='relu'),  # 全连接层,84个节点
    layers.Dense(10),  # 全连接层,10个节点
])

# build一次网格模型,给输入x的形状,其中4为随意给的batchsize
network.build(input_shape=(4, 28, 28, 1))
# 统计网络信息
network.summary()

# 创建损失函数的类,在实际计算时直接调用实例即可
criteon = losses.CategoricalCrossentropy(from_logits=True)

optimizer = optimizers.Adam(lr=0.01)

# 训练部分实现如下
# 构建梯度记录环境
# 训练20个epoch


def train_epoch(epoch):
    for step, (x, y) in enumerate(train_db):  # 循环优化
        with tf.GradientTape() as tape:
            # 插入通道维度,=>[b,28,28,1]
            x = tf.expand_dims(x, axis=3)
            # 向前计算,获得10类别的概率分布,[b,784] => [b,10]
            out = network(x)
            # 真实标签one-hot编码,[b] => [b,10]
            y_onehot = tf.one_hot(y, depth=10)
            # 计算交叉熵损失函数,标量
            loss = criteon(y_onehot, out)
        # 自动计算梯度
        grads = tape.gradient(loss, network.trainable_variables)
        # 自动更新参数
        optimizer.apply_gradients(zip(grads, network.trainable_variables))
        if step % 100 == 0:
            print(step, 'loss:', loss_meter.result().numpy())
            loss_meter.reset_states()

        # 计算准确度
        if step % 100 == 0:
            # 记录预测正确的数量,总样本数量
            correct, total = 0, 0
            for x, y in test_db:  # 遍历所有训练集样本
                # 插入通道维度,=>[b,28,28,1]
                x = tf.expand_dims(x, axis=3)
                # 向前计算,获得10类别的概率分布,[b,784] => [b,10]
                out = network(x)
                # 真实的流程时先经过softmax,再argmax
                # 但是由于softmax不改变元素的大小相对关系,故省去
                pred = tf.argmax(out,axis=-1)
                y = tf.cast(y, tf.int64)
                # 统计预测样本总数
                correct += float(tf.reduce_sum(tf.cast(tf.equal(pred, y), tf.float32)))
                # 统计预测样本总数
                total += x.shape[0]
            # 计算准确率
            print('test acc:', correct/total)

def train():

    for epoch in range(30):

        train_epoch(epoch)

if __name__ == '__main__':
    train()
上一篇:【tensorflow】linux安装tensorflow,和cuda, cudnn版本对应关系


下一篇:深度学习之基于Tensorflow2.0实现VGG16网络